new graph

This commit is contained in:
Shoubhit Dash 2026-01-16 12:36:57 +05:30
parent d0d7d28a8d
commit 475bdf63ff
126 changed files with 8281 additions and 2761 deletions

View file

@ -209,7 +209,6 @@ function addSupermemoryButtonToMemoriesDialog() {
if (memoriesDialog.querySelector("#supermemory-save-button")) return
const deleteAllContainer = memoriesDialog.querySelector(
".flex.items-center.gap-0\\.5",
)

View file

@ -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

View file

@ -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
}

View file

@ -92,7 +92,6 @@ async function handleAllBookmarksImportClick() {
}
}
async function showAllBookmarksProjectModal(
projects: Array<{ id: string; name: string; containerTag: string }>,
) {

View file

@ -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)
}

View file

@ -37,7 +37,6 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
initPosthog(this.env.POSTHOG_API_KEY)
// Hook MCP McpAgent to capture client info
this.server.server.oninitialized = async () => {
const clientVersion = this.server.server.getClientVersion()

View file

@ -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"]
}

View file

@ -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": [
{

View file

@ -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

View file

@ -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 apiKey = getApiKey()
const url = `${API_BASE_URL}${endpoint}`;
const url = `${API_BASE_URL}${endpoint}`
try {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
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",
);
}
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
}
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);
}
throw new SupermemoryAPIError(errorMessage, response.status)
}
if (!response.headers.get("content-type")?.includes("application/json")) {
throw new SupermemoryAPIError("Invalid response format from API");
}
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;
}
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"}`,
);
}
// 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),
});
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",
});
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;
}
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),
},
);
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;
}
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
}

View file

@ -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>
)
}

View file

@ -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} />
}
}

View file

@ -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
}
}

View file

@ -1,2 +1,2 @@
NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_KEY=

View file

@ -0,0 +1,56 @@
"use client"
import { useAuth } from "@lib/auth-context"
import { LoaderIcon } from "lucide-react"
import { Graph } from "@/components/graph"
import { useProject } from "@/stores"
import { AddMemoryView } from "@/components/views/add-memory"
import { useState } from "react"
export default function GraphPage() {
const { user } = useAuth()
const { selectedProject } = useProject()
const [showAddMemory, setShowAddMemory] = useState(false)
if (!user) {
return (
<div className="h-screen flex items-center justify-center bg-slate-900">
<div className="flex flex-col items-center gap-4">
<LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
<p className="text-white/60">Loading...</p>
</div>
</div>
)
}
const containerTags =
selectedProject && selectedProject !== "sm_project_default"
? [selectedProject]
: undefined
return (
<div className="h-full w-full bg-slate-900">
<Graph containerTags={containerTags}>
<div className="absolute inset-0 flex items-center justify-center">
<div className="rounded-xl overflow-hidden p-6 text-center">
<p className="text-slate-400 mb-4">No memories found</p>
<button
type="button"
onClick={() => setShowAddMemory(true)}
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
>
Add your first memory
</button>
</div>
</div>
</Graph>
{showAddMemory && (
<AddMemoryView
initialTab="note"
onClose={() => setShowAddMemory(false)}
/>
)}
</div>
)
}

View file

@ -46,7 +46,7 @@ export default function NavigationLayout({
<div className="sticky top-0 z-50 bg-background/80 backdrop-blur-md border-b border-white/10">
<Header onAddMemory={() => setShowAddMemoryView(true)} />
</div>
<div className="flex-1">{children}</div>
<div className="flex-1 overflow-hidden">{children}</div>
{showAddMemoryView && (
<AddMemoryView
initialTab="note"

View file

@ -33,7 +33,10 @@ export default function Page() {
if (sessionToken && userData?.email) {
const encodedToken = encodeURIComponent(sessionToken)
window.postMessage({ token: encodedToken, userData }, window.location.origin)
window.postMessage(
{ token: encodedToken, userData },
window.location.origin,
)
url.searchParams.delete("extension-auth-success")
window.history.replaceState({}, "", url.toString())
}

View file

@ -9,4 +9,4 @@ export default function BillingPage() {
<BillingView />
</div>
)
}
}

View file

@ -7,4 +7,4 @@ export default function IntegrationsPage() {
<IntegrationsView />
</div>
)
}
}

View file

@ -9,4 +9,4 @@ export default function ProfilePage() {
<ProfileView />
</div>
)
}
}

View file

@ -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,
},
);
)
}

View file

@ -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",
},
],
};
}
}

View file

@ -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>
);
)
}

View file

@ -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

View file

@ -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)));
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 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 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 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 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]);
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}
/>
);
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;
};
}>>([]);
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);
useEffect(() => {
setMounted(true)
const screenWidth = typeof window !== "undefined" ? window.innerWidth : 1200;
const screenHeight = typeof window !== "undefined" ? window.innerHeight : 800;
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 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)"
}
];
// 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
// 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;
}
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
};
});
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);
}, []);
setOrbs(newOrbs)
}, [])
if (!mounted || orbs.length === 0) return null;
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>
);
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>
)
}

View file

@ -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>
);
}
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>
)
}

View file

@ -56,7 +56,7 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
// Check if it's a profile link (not a status/tweet link)
const profilePattern =
/^(https?:\/\/)?(www\.)?(x\.com|twitter\.com)\/[^\/]+$/
/^(https?:\/\/)?(www\.)?(x\.com|twitter\.com)\/[^/]+$/
const statusPattern = /\/status\//i
if (statusPattern.test(normalized) || !profilePattern.test(normalized)) {
@ -80,7 +80,7 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
// Check if it's a profile link (should have /in/ or /pub/)
const profilePattern =
/^(https?:\/\/)?(www\.)?linkedin\.com\/(in|pub)\/[^\/]+/
/^(https?:\/\/)?(www\.)?linkedin\.com\/(in|pub)\/[^/]+/
if (!profilePattern.test(normalized)) {
return "share your Linkedin profile link"

View file

@ -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>
);
)
}

View file

@ -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>
);
)
}

View file

@ -16,4 +16,4 @@
}
}
}
}
}

View file

@ -1,227 +1,226 @@
'use client';
"use client"
import {
motion,
useMotionValue,
useTransform,
animate,
useReducedMotion,
} from 'motion/react';
motion,
useMotionValue,
useTransform,
animate,
useReducedMotion,
} from "motion/react"
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo } from "react"
import * as flubber from 'flubber';
import * as flubber from "flubber"
type ChatLoaderProps = {
size?: number;
colorClassName?: string;
label?: string;
className?: string;
};
const LEFT_PATHS = [
'M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H19.0324V9.02914L12.6984 9.02793Z', // 0
'M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H14.149L12.699 9.02914L12.6984 9.02793Z', // 1
'M12.6985 9.02793V3.52344H10.6524V9.49591C10.6524 10.1302 10.6516 10.7381 10.6532 11.0926L10.6524 16.4075H12.6985L12.6991 11.0926V9.02914L12.6985 9.02793Z', // 2
'M14.5653 7.14453V7.1485H10.6528V8.0394C10.6528 9.25237 10.6512 10.4147 10.6542 11.0925L10.6528 11.0887H14.5653L14.5664 11.0925V7.14684L14.5653 7.14453Z', // 3
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 4
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 5
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 6
'M19.0304 6.51562V6.51963H15.0776V7.41971C15.0776 8.64517 15.076 9.81944 15.0791 10.5043L15.0776 10.5004H19.0304L19.0315 10.5043V6.51796L19.0304 6.51562Z', // 7
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 8
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 9
'M19.0304 6.51562V6.51963H15.0776V7.41971C15.0776 8.64517 15.076 9.81944 15.0791 10.5043L15.0776 10.5004H19.0304L19.0315 10.5043V6.51796L19.0304 6.51562Z', // 10
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 11
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 12
'M19.0304 6.51562V6.51963H15.0776V7.41971C15.0776 8.64517 15.076 9.81944 15.0791 10.5043L15.0776 10.5004H19.0304L19.0315 10.5043V6.51796L19.0304 6.51562Z', // 13
'M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z', // 14
'M14.5653 7.14453V7.1485H10.6528V8.0394C10.6528 9.25237 10.6512 10.4147 10.6542 11.0925L10.6528 11.0887H14.5653L14.5664 11.0925V7.14684L14.5653 7.14453Z', // 15
'M12.6985 9.02793V3.52344H10.6524V9.49591C10.6524 10.1302 10.6516 10.7381 10.6532 11.0926L10.6524 16.4075H12.6985L12.6991 11.0926V9.02914L12.6985 9.02793Z', // 16
'M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H14.149L12.699 9.02914L12.6984 9.02793Z', // 17
'M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H19.0324V9.02914L12.6984 9.02793Z', // 18
];
const MIDDLE_PATHS = [
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 0
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 1
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 2
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 3
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 4
'M1.96875 6.50391L1.96904 10.4909L1.9702 10.4833H5.92302V9.58685C5.92302 8.3613 5.92302 7.73358 5.92302 6.50459L1.96875 6.50391Z', // 5
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 6
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 7
'M1.96875 6.50391L1.96904 10.4909L1.9702 10.4833H5.92302V9.58685C5.92302 8.3613 5.92302 7.73358 5.92302 6.50459L1.96875 6.50391Z', // 8
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 9
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 10
'M1.96875 6.50391L1.96904 10.4909L1.9702 10.4833H5.92302V9.58685C5.92302 8.3613 5.92302 7.73358 5.92302 6.50459L1.96875 6.50391Z', // 11
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 12
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 13
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 14
'M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z', // 15
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 16
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 17
'M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z', // 18
];
const RIGHT_PATHS = [
'M3.03472 6.05861L6.8539 9.91021H1.96777V11.9737H8.3006V17.4781H10.3467V11.5057C10.3467 10.8713 10.0963 10.2621 9.65119 9.81327L4.48145 4.59961L3.03472 6.05861Z', // 0
'M3.03516 6.05861L6.85434 9.91021H6.85044L8.30044 11.9737L8.30104 17.4781H10.3471V11.5057C10.3471 10.8713 10.0967 10.2621 9.65162 9.81327L4.48188 4.59961L3.03516 6.05861Z', // 1
'M8.30024 4.58789L8.2998 9.91036L8.30039 11.9738L8.30099 17.4783H10.3471V11.5058C10.3471 10.8714 10.3471 10.5465 10.3471 9.91036V4.58789H8.30024Z', // 2
'M6.42383 9.9082L6.42412 13.8633L6.42527 13.8557H10.3464V12.9664C10.3464 11.7507 10.3464 11.128 10.3464 9.90888L6.42383 9.9082Z', // 3
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 4
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 5
'M8.52051 6.50391L8.5208 10.4909L8.52196 10.4833H12.4748V9.58685C12.4748 8.3613 12.4748 7.73358 12.4748 6.50459L8.52051 6.50391Z', // 6
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 7
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 8
'M8.52051 6.50391L8.5208 10.4909L8.52196 10.4833H12.4748V9.58685C12.4748 8.3613 12.4748 7.73358 12.4748 6.50459L8.52051 6.50391Z', // 9
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 10
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 11
'M8.52051 6.50391L8.5208 10.4909L8.52196 10.4833H12.4748V9.58685C12.4748 8.3613 12.4748 7.73358 12.4748 6.50459L8.52051 6.50391Z', // 12
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 13
'M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z', // 14
'M6.42383 9.9082L6.42412 13.8633L6.42527 13.8557H10.3464V12.9664C10.3464 11.7507 10.3464 11.128 10.3464 9.90888L6.42383 9.9082Z', // 15
'M8.30024 4.58789L8.2998 9.91036L8.30039 11.9738L8.30099 17.4783H10.3471V11.5058C10.3471 10.8714 10.3471 10.5465 10.3471 9.91036V4.58789H8.30024Z', // 16
'M3.03516 6.05861L6.85434 9.91021H6.85044L8.30044 11.9737L8.30104 17.4781H10.3471V11.5057C10.3471 10.8713 10.0967 10.2621 9.65162 9.81327L4.48188 4.59961L3.03516 6.05861Z', // 17
'M3.03472 6.05861L6.8539 9.91021H1.96777V11.9737H8.3006V17.4781H10.3467V11.5057C10.3467 10.8713 10.0963 10.2621 9.65119 9.81327L4.48145 4.59961L3.03472 6.05861Z', // 18
];
export function ChatLoader({
size = 80,
colorClassName = 'text-white',
label = '',
className = '',
}: ChatLoaderProps) {
const prefersReducedMotion = useReducedMotion();
const t = useMotionValue(0);
const loopDuration = 3.6; // full cycle
const makeMultiInterp = (paths: string[]) => {
if (!paths || paths.length === 0) {
return (_t: number) => '';
}
if (paths.length === 1) {
const only = paths[0];
return (_t: number) => only;
}
const options: { maxSegmentLength?: number } = { maxSegmentLength: 0.5 };
const interpolateFn = (flubber as any).interpolate as (
from: string,
to: string,
options?: { maxSegmentLength?: number }
) => (t: number) => string;
const segmentInterpolators: Array<(t: number) => string> = [];
for (let i = 0; i < paths.length - 1; i++) {
segmentInterpolators.push(interpolateFn(paths[i], paths[i + 1], options));
}
const segmentCount = segmentInterpolators.length;
return (t: number) => {
if (t <= 0) return paths[0] || '';
if (t >= 1) return paths[paths.length - 1] || '';
const scaled = t * segmentCount;
const segIndex = Math.min(Math.floor(scaled), segmentCount - 1);
const localT = scaled - segIndex;
return segmentInterpolators[segIndex](localT);
};
};
const leftInterp = useMemo(
() => (LEFT_PATHS.length ? makeMultiInterp(LEFT_PATHS) : null),
[]
);
const middleInterp = useMemo(
() => (MIDDLE_PATHS.length ? makeMultiInterp(MIDDLE_PATHS) : null),
[]
);
const rightInterp = useMemo(
() => (RIGHT_PATHS.length ? makeMultiInterp(RIGHT_PATHS) : null),
[]
);
// Turn scalar t into d strings
const leftD = useTransform(t, v =>
leftInterp ? leftInterp(v) : LEFT_PATHS[0] || ''
);
const middleD = useTransform(t, v =>
middleInterp ? middleInterp(v) : MIDDLE_PATHS[0] || ''
);
const rightD = useTransform(t, v =>
rightInterp ? rightInterp(v) : RIGHT_PATHS[0] || ''
);
const middleOpacity = useTransform(t, v => {
if (v < 0.2) return 0;
if (v < 0.3) return (v - 0.2) / 0.1; // fade in
if (v < 0.8) return 1;
if (v < 0.9) return 1 - (v - 0.8) / 0.1; // fade out
return 0;
});
useEffect(() => {
if (prefersReducedMotion) {
t.set(0);
return;
}
const controls = animate(t, [0, 1], {
duration: loopDuration,
ease: 'linear',
repeat: Infinity,
repeatType: 'loop',
repeatDelay: 0.4, // ⬅️ wait 2 seconds at the end before restarting
});
return () => controls.stop();
}, [t, prefersReducedMotion, loopDuration]);
return (
<div
role='status'
aria-label={label}
className={`inline-flex flex-col items-center gap-2 ${className}`}
style={{ width: size }}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 36 20'
width={size}
height={(size * 20) / 36}
className={colorClassName}
>
{leftInterp && <motion.path d={leftD as any} fill='currentColor' />}
{rightInterp && <motion.path d={rightD as any} fill='currentColor' />}
{middleInterp && (
<motion.path
d={middleD as any}
fill='currentColor'
style={{ opacity: middleOpacity as any }}
/>
)}
</svg>
{label && (
<span
className='text-xs font-medium text-slate-400'
style={{ fontSize: size * 0.18 }}
>
{label}
</span>
)}
</div>
);
size?: number
colorClassName?: string
label?: string
className?: string
}
const LEFT_PATHS = [
"M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H19.0324V9.02914L12.6984 9.02793Z", // 0
"M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H14.149L12.699 9.02914L12.6984 9.02793Z", // 1
"M12.6985 9.02793V3.52344H10.6524V9.49591C10.6524 10.1302 10.6516 10.7381 10.6532 11.0926L10.6524 16.4075H12.6985L12.6991 11.0926V9.02914L12.6985 9.02793Z", // 2
"M14.5653 7.14453V7.1485H10.6528V8.0394C10.6528 9.25237 10.6512 10.4147 10.6542 11.0925L10.6528 11.0887H14.5653L14.5664 11.0925V7.14684L14.5653 7.14453Z", // 3
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 4
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 5
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 6
"M19.0304 6.51562V6.51963H15.0776V7.41971C15.0776 8.64517 15.076 9.81944 15.0791 10.5043L15.0776 10.5004H19.0304L19.0315 10.5043V6.51796L19.0304 6.51562Z", // 7
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 8
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 9
"M19.0304 6.51562V6.51963H15.0776V7.41971C15.0776 8.64517 15.076 9.81944 15.0791 10.5043L15.0776 10.5004H19.0304L19.0315 10.5043V6.51796L19.0304 6.51562Z", // 10
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 11
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 12
"M19.0304 6.51562V6.51963H15.0776V7.41971C15.0776 8.64517 15.076 9.81944 15.0791 10.5043L15.0776 10.5004H19.0304L19.0315 10.5043V6.51796L19.0304 6.51562Z", // 13
"M19.0304 8.51562V8.51963H15.0776V9.41971C15.0776 10.6452 15.076 11.8194 15.0791 12.5043L15.0776 12.5004H19.0304L19.0315 12.5043V8.51796L19.0304 8.51562Z", // 14
"M14.5653 7.14453V7.1485H10.6528V8.0394C10.6528 9.25237 10.6512 10.4147 10.6542 11.0925L10.6528 11.0887H14.5653L14.5664 11.0925V7.14684L14.5653 7.14453Z", // 15
"M12.6985 9.02793V3.52344H10.6524V9.49591C10.6524 10.1302 10.6516 10.7381 10.6532 11.0926L10.6524 16.4075H12.6985L12.6991 11.0926V9.02914L12.6985 9.02793Z", // 16
"M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H14.149L12.699 9.02914L12.6984 9.02793Z", // 17
"M12.6984 9.02793V3.52344H10.6523V9.49591C10.6523 10.1302 10.9028 10.7395 11.3479 11.1883L16.5188 16.4032L17.9655 14.9441L14.1463 11.0926H19.0324V9.02914L12.6984 9.02793Z", // 18
]
const MIDDLE_PATHS = [
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 0
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 1
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 2
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 3
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 4
"M1.96875 6.50391L1.96904 10.4909L1.9702 10.4833H5.92302V9.58685C5.92302 8.3613 5.92302 7.73358 5.92302 6.50459L1.96875 6.50391Z", // 5
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 6
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 7
"M1.96875 6.50391L1.96904 10.4909L1.9702 10.4833H5.92302V9.58685C5.92302 8.3613 5.92302 7.73358 5.92302 6.50459L1.96875 6.50391Z", // 8
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 9
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 10
"M1.96875 6.50391L1.96904 10.4909L1.9702 10.4833H5.92302V9.58685C5.92302 8.3613 5.92302 7.73358 5.92302 6.50459L1.96875 6.50391Z", // 11
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 12
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 13
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 14
"M1.96875 8.50391L1.96904 12.4909L1.9702 12.4833H5.92302V11.5868C5.92302 10.3613 5.92302 9.73358 5.92302 8.50459L1.96875 8.50391Z", // 15
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 16
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 17
"M6.60156 9.46875L6.60171 11.5326L6.60231 11.5286H8.64841V11.0646C8.64841 10.4302 8.64841 10.1053 8.64841 9.46911L6.60156 9.46875Z", // 18
]
const RIGHT_PATHS = [
"M3.03472 6.05861L6.8539 9.91021H1.96777V11.9737H8.3006V17.4781H10.3467V11.5057C10.3467 10.8713 10.0963 10.2621 9.65119 9.81327L4.48145 4.59961L3.03472 6.05861Z", // 0
"M3.03516 6.05861L6.85434 9.91021H6.85044L8.30044 11.9737L8.30104 17.4781H10.3471V11.5057C10.3471 10.8713 10.0967 10.2621 9.65162 9.81327L4.48188 4.59961L3.03516 6.05861Z", // 1
"M8.30024 4.58789L8.2998 9.91036L8.30039 11.9738L8.30099 17.4783H10.3471V11.5058C10.3471 10.8714 10.3471 10.5465 10.3471 9.91036V4.58789H8.30024Z", // 2
"M6.42383 9.9082L6.42412 13.8633L6.42527 13.8557H10.3464V12.9664C10.3464 11.7507 10.3464 11.128 10.3464 9.90888L6.42383 9.9082Z", // 3
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 4
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 5
"M8.52051 6.50391L8.5208 10.4909L8.52196 10.4833H12.4748V9.58685C12.4748 8.3613 12.4748 7.73358 12.4748 6.50459L8.52051 6.50391Z", // 6
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 7
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 8
"M8.52051 6.50391L8.5208 10.4909L8.52196 10.4833H12.4748V9.58685C12.4748 8.3613 12.4748 7.73358 12.4748 6.50459L8.52051 6.50391Z", // 9
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 10
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 11
"M8.52051 6.50391L8.5208 10.4909L8.52196 10.4833H12.4748V9.58685C12.4748 8.3613 12.4748 7.73358 12.4748 6.50459L8.52051 6.50391Z", // 12
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 13
"M8.52051 8.50391L8.5208 12.4909L8.52196 12.4833H12.4748V11.5868C12.4748 10.3613 12.4748 9.73358 12.4748 8.50459L8.52051 8.50391Z", // 14
"M6.42383 9.9082L6.42412 13.8633L6.42527 13.8557H10.3464V12.9664C10.3464 11.7507 10.3464 11.128 10.3464 9.90888L6.42383 9.9082Z", // 15
"M8.30024 4.58789L8.2998 9.91036L8.30039 11.9738L8.30099 17.4783H10.3471V11.5058C10.3471 10.8714 10.3471 10.5465 10.3471 9.91036V4.58789H8.30024Z", // 16
"M3.03516 6.05861L6.85434 9.91021H6.85044L8.30044 11.9737L8.30104 17.4781H10.3471V11.5057C10.3471 10.8713 10.0967 10.2621 9.65162 9.81327L4.48188 4.59961L3.03516 6.05861Z", // 17
"M3.03472 6.05861L6.8539 9.91021H1.96777V11.9737H8.3006V17.4781H10.3467V11.5057C10.3467 10.8713 10.0963 10.2621 9.65119 9.81327L4.48145 4.59961L3.03472 6.05861Z", // 18
]
export function ChatLoader({
size = 80,
colorClassName = "text-white",
label = "",
className = "",
}: ChatLoaderProps) {
const prefersReducedMotion = useReducedMotion()
const t = useMotionValue(0)
const loopDuration = 3.6 // full cycle
const makeMultiInterp = (paths: string[]) => {
if (!paths || paths.length === 0) {
return (_t: number) => ""
}
if (paths.length === 1) {
const only = paths[0]
return (_t: number) => only
}
const options: { maxSegmentLength?: number } = { maxSegmentLength: 0.5 }
const interpolateFn = (flubber as any).interpolate as (
from: string,
to: string,
options?: { maxSegmentLength?: number },
) => (t: number) => string
const segmentInterpolators: Array<(t: number) => string> = []
for (let i = 0; i < paths.length - 1; i++) {
segmentInterpolators.push(interpolateFn(paths[i], paths[i + 1], options))
}
const segmentCount = segmentInterpolators.length
return (t: number) => {
if (t <= 0) return paths[0] || ""
if (t >= 1) return paths[paths.length - 1] || ""
const scaled = t * segmentCount
const segIndex = Math.min(Math.floor(scaled), segmentCount - 1)
const localT = scaled - segIndex
return segmentInterpolators[segIndex](localT)
}
}
const leftInterp = useMemo(
() => (LEFT_PATHS.length ? makeMultiInterp(LEFT_PATHS) : null),
[],
)
const middleInterp = useMemo(
() => (MIDDLE_PATHS.length ? makeMultiInterp(MIDDLE_PATHS) : null),
[],
)
const rightInterp = useMemo(
() => (RIGHT_PATHS.length ? makeMultiInterp(RIGHT_PATHS) : null),
[],
)
// Turn scalar t into d strings
const leftD = useTransform(t, (v) =>
leftInterp ? leftInterp(v) : LEFT_PATHS[0] || "",
)
const middleD = useTransform(t, (v) =>
middleInterp ? middleInterp(v) : MIDDLE_PATHS[0] || "",
)
const rightD = useTransform(t, (v) =>
rightInterp ? rightInterp(v) : RIGHT_PATHS[0] || "",
)
const middleOpacity = useTransform(t, (v) => {
if (v < 0.2) return 0
if (v < 0.3) return (v - 0.2) / 0.1 // fade in
if (v < 0.8) return 1
if (v < 0.9) return 1 - (v - 0.8) / 0.1 // fade out
return 0
})
useEffect(() => {
if (prefersReducedMotion) {
t.set(0)
return
}
const controls = animate(t, [0, 1], {
duration: loopDuration,
ease: "linear",
repeat: Number.POSITIVE_INFINITY,
repeatType: "loop",
repeatDelay: 0.4, // ⬅️ wait 2 seconds at the end before restarting
})
return () => controls.stop()
}, [t, prefersReducedMotion, loopDuration])
return (
<div
role="status"
aria-label={label}
className={`inline-flex flex-col items-center gap-2 ${className}`}
style={{ width: size }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 36 20"
width={size}
height={(size * 20) / 36}
className={colorClassName}
>
{leftInterp && <motion.path d={leftD as any} fill="currentColor" />}
{rightInterp && <motion.path d={rightD as any} fill="currentColor" />}
{middleInterp && (
<motion.path
d={middleD as any}
fill="currentColor"
style={{ opacity: middleOpacity as any }}
/>
)}
</svg>
{label && (
<span
className="text-xs font-medium text-slate-400"
style={{ fontSize: size * 0.18 }}
>
{label}
</span>
)}
</div>
)
}

View file

@ -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>
);
)
}

View file

@ -0,0 +1,67 @@
export const colors = {
background: {
primary: "#0f1419",
secondary: "#1a1f29",
accent: "#252a35",
},
document: {
primary: "rgba(255, 255, 255, 0.21)",
secondary: "rgba(255, 255, 255, 0.31)",
accent: "rgba(255, 255, 255, 0.31)",
border: "rgba(255, 255, 255, 0.6)",
glow: "rgba(147, 197, 253, 0.4)",
},
memory: {
primary: "rgba(147, 196, 253, 0.21)",
secondary: "rgba(147, 196, 253, 0.31)",
accent: "rgba(147, 197, 253, 0.31)",
border: "rgba(147, 196, 253, 0.6)",
glow: "rgba(147, 197, 253, 0.5)",
},
connection: {
weak: "rgba(35, 189, 255, 0.3)",
memory: "rgba(148, 163, 184, 0.35)",
medium: "rgba(35, 189, 255, 0.6)",
strong: "rgba(35, 189, 255, 0.9)",
},
text: {
primary: "#ffffff",
secondary: "#e2e8f0",
muted: "#94a3b8",
},
accent: {
primary: "rgba(59, 130, 246, 0.7)",
secondary: "rgba(99, 102, 241, 0.6)",
glow: "rgba(147, 197, 253, 0.6)",
amber: "rgba(251, 165, 36, 0.8)",
emerald: "rgba(16, 185, 129, 0.4)",
},
status: {
forgotten: "rgba(220, 38, 38, 0.15)",
expiring: "rgba(251, 165, 36, 0.8)",
new: "rgba(16, 185, 129, 0.4)",
},
relations: {
updates: "rgba(147, 77, 253, 0.5)",
extends: "rgba(16, 185, 129, 0.5)",
derives: "rgba(147, 197, 253, 0.5)",
},
}
export const GRAPH_SETTINGS = {
initialZoom: 0.8,
initialPanX: 0,
initialPanY: 0,
}
export const ANIMATION = {
dimDuration: 1500,
}
export const NODE_SIZES = {
document: 58,
memory: 40,
}
export const COORDINATE_SCALE = 15
export const MEMORY_ORBIT_RADIUS = 80

View file

@ -0,0 +1,204 @@
export type DocumentIconType =
| "text"
| "pdf"
| "md"
| "markdown"
| "docx"
| "doc"
| "rtf"
| "csv"
| "json"
export function drawDocumentIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
type: string,
color = "rgba(255, 255, 255, 0.9)",
): void {
ctx.save()
ctx.fillStyle = color
ctx.strokeStyle = color
ctx.lineWidth = Math.max(1, size / 12)
ctx.lineCap = "round"
ctx.lineJoin = "round"
switch (type) {
case "pdf":
drawPdfIcon(ctx, x, y, size)
break
case "md":
case "markdown":
drawMarkdownIcon(ctx, x, y, size)
break
case "doc":
case "docx":
drawWordIcon(ctx, x, y, size)
break
case "rtf":
drawRtfIcon(ctx, x, y, size)
break
case "csv":
drawCsvIcon(ctx, x, y, size)
break
case "json":
drawJsonIcon(ctx, x, y, size)
break
case "txt":
case "text":
default:
drawTextIcon(ctx, x, y, size)
break
}
ctx.restore()
}
function drawTextIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
const cornerFold = size * 0.2
ctx.beginPath()
ctx.moveTo(x - w / 2, y - h / 2)
ctx.lineTo(x + w / 2 - cornerFold, y - h / 2)
ctx.lineTo(x + w / 2, y - h / 2 + cornerFold)
ctx.lineTo(x + w / 2, y + h / 2)
ctx.lineTo(x - w / 2, y + h / 2)
ctx.closePath()
ctx.stroke()
const lineSpacing = size * 0.15
const lineWidth = size * 0.4
ctx.beginPath()
ctx.moveTo(x - lineWidth / 2, y - lineSpacing)
ctx.lineTo(x + lineWidth / 2, y - lineSpacing)
ctx.moveTo(x - lineWidth / 2, y)
ctx.lineTo(x + lineWidth / 2, y)
ctx.moveTo(x - lineWidth / 2, y + lineSpacing)
ctx.lineTo(x + lineWidth / 2, y + lineSpacing)
ctx.stroke()
}
function drawPdfIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.35}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("PDF", x, y)
}
function drawMarkdownIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.3}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("MD", x, y)
}
function drawWordIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.28}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("DOC", x, y)
}
function drawRtfIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.3}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("RTF", x, y)
}
function drawCsvIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.strokeRect(x - w / 2, y - h / 2, w, h)
ctx.beginPath()
ctx.moveTo(x, y - h / 2)
ctx.lineTo(x, y + h / 2)
ctx.moveTo(x - w / 2, y)
ctx.lineTo(x + w / 2, y)
ctx.stroke()
}
function drawJsonIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.6
const h = size * 0.8
ctx.beginPath()
ctx.moveTo(x - w / 4, y - h / 2)
ctx.quadraticCurveTo(x - w / 2, y - h / 3, x - w / 2, y)
ctx.quadraticCurveTo(x - w / 2, y + h / 3, x - w / 4, y + h / 2)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x + w / 4, y - h / 2)
ctx.quadraticCurveTo(x + w / 2, y - h / 3, x + w / 2, y)
ctx.quadraticCurveTo(x + w / 2, y + h / 3, x + w / 4, y + h / 2)
ctx.stroke()
}

View file

@ -0,0 +1,760 @@
"use client"
import {
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import { colors, ANIMATION } from "./constants"
import type { GraphCanvasProps, GraphNode } from "./types"
import type { ViewportDocument, ViewportMemoryEntry } from "@/lib/viewport-graph-types"
import { drawDocumentIcon } from "./document-icons"
export const GraphCanvas = memo<GraphCanvasProps>(
({
nodes,
edges,
panX,
panY,
zoom,
width,
height,
onNodeHover,
onNodeClick,
onPanStart,
onPanMove,
onPanEnd,
onWheel,
onDoubleClick,
onTouchStart,
onTouchMove,
onTouchEnd,
highlightDocumentIds,
selectedNodeId = null,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animationRef = useRef<number>(0)
const startTimeRef = useRef<number>(Date.now())
const mousePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const currentHoveredNode = useRef<string | null>(null)
const dimProgress = useRef<number>(selectedNodeId ? 1 : 0)
const dimAnimationRef = useRef<number>(0)
const [, forceRender] = useState(0)
useEffect(() => {
startTimeRef.current = Date.now()
}, [])
useLayoutEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = "high"
}, [])
useEffect(() => {
const targetDim = selectedNodeId ? 1 : 0
const duration = ANIMATION.dimDuration
const startDim = dimProgress.current
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
const eased = 1 - (1 - progress) ** 3
dimProgress.current = startDim + (targetDim - startDim) * eased
forceRender((prev) => prev + 1)
if (progress < 1) {
dimAnimationRef.current = requestAnimationFrame(animate)
}
}
if (dimAnimationRef.current) {
cancelAnimationFrame(dimAnimationRef.current)
}
animate()
return () => {
if (dimAnimationRef.current) {
cancelAnimationFrame(dimAnimationRef.current)
}
}
}, [selectedNodeId])
const spatialGrid = useMemo(() => {
const GRID_CELL_SIZE = 150
const grid = new Map<string, GraphNode[]>()
for (const node of nodes) {
const screenX = node.x * zoom + panX
const screenY = node.y * zoom + panY
const cellX = Math.floor(screenX / GRID_CELL_SIZE)
const cellY = Math.floor(screenY / GRID_CELL_SIZE)
const cellKey = `${cellX},${cellY}`
if (!grid.has(cellKey)) {
grid.set(cellKey, [])
}
grid.get(cellKey)!.push(node)
}
return { grid, cellSize: GRID_CELL_SIZE }
}, [nodes, panX, panY, zoom])
const getNodeAtPosition = useCallback(
(x: number, y: number): string | null => {
const { grid, cellSize } = spatialGrid
const cellX = Math.floor(x / cellSize)
const cellY = Math.floor(y / cellSize)
const cellKey = `${cellX},${cellY}`
const cellsToCheck = [
cellKey,
`${cellX - 1},${cellY}`,
`${cellX + 1},${cellY}`,
`${cellX},${cellY - 1}`,
`${cellX},${cellY + 1}`,
]
for (const key of cellsToCheck) {
const cellNodes = grid.get(key)
if (!cellNodes) continue
for (let i = cellNodes.length - 1; i >= 0; i--) {
const node = cellNodes[i]!
const screenX = node.x * zoom + panX
const screenY = node.y * zoom + panY
const nodeSize = node.size * zoom
if (node.type === "document") {
const docWidth = nodeSize * 1.4
const docHeight = nodeSize * 0.9
const halfW = docWidth / 2
const halfH = docHeight / 2
if (
x >= screenX - halfW &&
x <= screenX + halfW &&
y >= screenY - halfH &&
y <= screenY + halfH
) {
return node.id
}
} else {
const dx = x - screenX
const dy = y - screenY
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance <= nodeSize / 2) {
return node.id
}
}
}
}
return null
},
[spatialGrid, panX, panY, zoom],
)
const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
mousePos.current = { x, y }
const nodeId = getNodeAtPosition(x, y)
if (nodeId !== currentHoveredNode.current) {
currentHoveredNode.current = nodeId
onNodeHover(nodeId)
}
},
[getNodeAtPosition, onNodeHover],
)
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
onPanStart(e)
},
[onPanStart],
)
const handleClick = useCallback(
(e: React.MouseEvent) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const nodeId = getNodeAtPosition(x, y)
if (nodeId) {
onNodeClick(nodeId)
}
},
[getNodeAtPosition, onNodeClick],
)
const nodeMap = useMemo(() => {
return new Map(nodes.map((node) => [node.id, node]))
}, [nodes])
const render = useCallback(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const useSimplifiedRendering = zoom < 0.3
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = "rgba(148, 163, 184, 0.03)"
ctx.lineWidth = 1
const gridSpacing = 100 * zoom
const offsetX = panX % gridSpacing
const offsetY = panY % gridSpacing
for (let x = offsetX; x < width; x += gridSpacing) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, height)
ctx.stroke()
}
for (let y = offsetY; y < height; y += gridSpacing) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(width, y)
ctx.stroke()
}
ctx.lineCap = "round"
const docMemoryEdges: typeof edges = []
const docDocEdges: typeof edges = []
for (const edge of edges) {
const sourceNode =
typeof edge.source === "string"
? nodeMap.get(edge.source)
: edge.source
const targetNode =
typeof edge.target === "string"
? nodeMap.get(edge.target)
: edge.target
if (sourceNode && targetNode) {
const sourceX = sourceNode.x * zoom + panX
const sourceY = sourceNode.y * zoom + panY
const targetX = targetNode.x * zoom + panX
const targetY = targetNode.y * zoom + panY
const edgeMargin = 100
if (
(sourceX < -edgeMargin && targetX < -edgeMargin) ||
(sourceX > width + edgeMargin && targetX > width + edgeMargin) ||
(sourceY < -edgeMargin && targetY < -edgeMargin) ||
(sourceY > height + edgeMargin && targetY > height + edgeMargin)
) {
continue
}
if (edge.edgeType === "doc-doc") {
docDocEdges.push(edge)
} else {
docMemoryEdges.push(edge)
}
}
}
const drawEdgePath = (
edge: (typeof edges)[0],
sourceNode: GraphNode,
targetNode: GraphNode,
) => {
const sourceX = sourceNode.x * zoom + panX
const sourceY = sourceNode.y * zoom + panY
const targetX = targetNode.x * zoom + panX
const targetY = targetNode.y * zoom + panY
if (useSimplifiedRendering) {
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
ctx.lineTo(targetX, targetY)
ctx.stroke()
} else {
const midX = (sourceX + targetX) / 2
const midY = (sourceY + targetY) / 2
const dx = targetX - sourceX
const dy = targetY - sourceY
const distance = Math.sqrt(dx * dx + dy * dy)
const controlOffset =
edge.edgeType === "doc-doc" ? Math.min(30, distance * 0.2) : 15
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
ctx.quadraticCurveTo(
midX + controlOffset * (dy / distance),
midY - controlOffset * (dx / distance),
targetX,
targetY,
)
ctx.stroke()
}
}
const edgeDimOpacity = 1 - dimProgress.current * 0.95
if (docMemoryEdges.length > 0) {
ctx.strokeStyle = colors.connection.memory
ctx.lineWidth = 1
ctx.setLineDash([])
for (const edge of docMemoryEdges) {
const sourceNode =
typeof edge.source === "string"
? nodeMap.get(edge.source)
: edge.source
const targetNode =
typeof edge.target === "string"
? nodeMap.get(edge.target)
: edge.target
if (sourceNode && targetNode) {
const edgeShouldDim =
selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.9
ctx.globalAlpha = opacity
drawEdgePath(edge, sourceNode, targetNode)
}
}
}
if (docDocEdges.length > 0) {
const dashPattern = useSimplifiedRendering ? [] : [10, 5]
ctx.setLineDash(dashPattern)
for (const edge of docDocEdges) {
const sourceNode =
typeof edge.source === "string"
? nodeMap.get(edge.source)
: edge.source
const targetNode =
typeof edge.target === "string"
? nodeMap.get(edge.target)
: edge.target
if (sourceNode && targetNode) {
const edgeShouldDim =
selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim
? edgeDimOpacity
: Math.max(0, edge.similarity * 0.5)
const lineWidth = Math.max(1, edge.similarity * 2)
let connectionColor = colors.connection.weak
if (edge.similarity > 0.85)
connectionColor = colors.connection.strong
else if (edge.similarity > 0.725)
connectionColor = colors.connection.medium
ctx.strokeStyle = connectionColor
ctx.lineWidth = lineWidth
ctx.globalAlpha = opacity
drawEdgePath(edge, sourceNode, targetNode)
}
}
}
ctx.globalAlpha = 1
ctx.setLineDash([])
const highlightSet = new Set<string>(highlightDocumentIds ?? [])
for (const node of nodes) {
const screenX = node.x * zoom + panX
const screenY = node.y * zoom + panY
const nodeSize = node.size * zoom
const margin = nodeSize + 50
if (
screenX < -margin ||
screenX > width + margin ||
screenY < -margin ||
screenY > height + margin
) {
continue
}
const isHovered = currentHoveredNode.current === node.id
const isSelected = selectedNodeId === node.id
const shouldDim = selectedNodeId !== null && !isSelected
const nodeOpacity = shouldDim ? 1 - dimProgress.current * 0.9 : 1
const isHighlightedDocument = (() => {
if (node.type !== "document" || highlightSet.size === 0) return false
const doc = node.data as ViewportDocument
if (doc.customId && highlightSet.has(doc.customId)) return true
return highlightSet.has(doc.id)
})()
if (node.type === "document") {
const docWidth = nodeSize * 1.4
const docHeight = nodeSize * 0.9
ctx.fillStyle = isHovered
? colors.document.secondary
: colors.document.primary
ctx.globalAlpha = nodeOpacity
ctx.strokeStyle = isHovered
? colors.document.accent
: colors.document.border
ctx.lineWidth = isHovered ? 2 : 1
const radius = useSimplifiedRendering ? 6 : 12
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2,
screenY - docHeight / 2,
docWidth,
docHeight,
radius,
)
ctx.fill()
ctx.stroke()
if (!useSimplifiedRendering && isHovered) {
ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"
ctx.lineWidth = 1
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2 + 1,
screenY - docHeight / 2 + 1,
docWidth - 2,
docHeight - 2,
radius - 1,
)
ctx.stroke()
}
if (isHighlightedDocument) {
ctx.save()
ctx.globalAlpha = 0.9
ctx.strokeStyle = colors.accent.primary
ctx.lineWidth = 3
ctx.setLineDash([6, 4])
const avgDimension = (docWidth + docHeight) / 2
const ringPadding = avgDimension * 0.1
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2 - ringPadding,
screenY - docHeight / 2 - ringPadding,
docWidth + ringPadding * 2,
docHeight + ringPadding * 2,
radius + 6,
)
ctx.stroke()
ctx.setLineDash([])
ctx.restore()
}
if (!useSimplifiedRendering) {
const doc = node.data as ViewportDocument
const iconSize = docHeight * 0.4
drawDocumentIcon(
ctx,
screenX,
screenY,
iconSize,
doc.type || "text",
"rgba(255, 255, 255, 0.8)",
)
}
} else {
const mem = node.data as ViewportMemoryEntry
const isNew =
new Date(mem.createdAt).getTime() > Date.now() - 1000 * 60 * 60 * 24
let fillColor = colors.memory.primary
let borderColor = colors.memory.border
if (isHovered) {
fillColor = colors.memory.secondary
}
if (isNew) {
borderColor = colors.status.new
}
const radius = nodeSize / 2
ctx.fillStyle = fillColor
ctx.globalAlpha = shouldDim ? nodeOpacity : 1
ctx.strokeStyle = borderColor
ctx.lineWidth = isHovered ? 2 : 1.5
if (useSimplifiedRendering) {
ctx.beginPath()
ctx.arc(screenX, screenY, radius, 0, 2 * Math.PI)
ctx.fill()
ctx.stroke()
} else {
const sides = 6
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
const x = screenX + radius * Math.cos(angle)
const y = screenY + radius * Math.sin(angle)
if (i === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.closePath()
ctx.fill()
ctx.stroke()
if (isHovered) {
ctx.strokeStyle = "rgba(147, 197, 253, 0.3)"
ctx.lineWidth = 1
const innerRadius = radius - 2
ctx.beginPath()
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
const x = screenX + innerRadius * Math.cos(angle)
const y = screenY + innerRadius * Math.sin(angle)
if (i === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.closePath()
ctx.stroke()
}
}
if (isNew) {
ctx.fillStyle = colors.status.new
ctx.beginPath()
ctx.arc(
screenX + nodeSize * 0.25,
screenY - nodeSize * 0.25,
Math.max(2, nodeSize * 0.15),
0,
2 * Math.PI,
)
ctx.fill()
}
}
if (!useSimplifiedRendering && isHovered) {
const glowColor =
node.type === "document" ? colors.document.glow : colors.memory.glow
ctx.strokeStyle = glowColor
ctx.lineWidth = 1
ctx.setLineDash([3, 3])
ctx.globalAlpha = 0.6
ctx.beginPath()
if (node.type === "document") {
const docWidth = nodeSize * 1.4
const docHeight = nodeSize * 0.9
const avgDimension = (docWidth + docHeight) / 2
const glowPadding = avgDimension * 0.1
ctx.roundRect(
screenX - docWidth / 2 - glowPadding,
screenY - docHeight / 2 - glowPadding,
docWidth + glowPadding * 2,
docHeight + glowPadding * 2,
15,
)
} else {
const glowRadius = nodeSize * 0.7
const sides = 6
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
const x = screenX + glowRadius * Math.cos(angle)
const y = screenY + glowRadius * Math.sin(angle)
if (i === 0) {
ctx.moveTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.closePath()
}
ctx.stroke()
ctx.setLineDash([])
}
}
ctx.globalAlpha = 1
}, [
nodes,
edges,
panX,
panY,
zoom,
width,
height,
highlightDocumentIds,
nodeMap,
selectedNodeId,
])
const lastRenderParams = useRef<number>(0)
const renderKey = useMemo(() => {
const positionHash = nodes.reduce((hash, n) => {
const x = Math.round(n.x * 10)
const y = Math.round(n.y * 10)
const hovered = currentHoveredNode.current === n.id ? 1 : 0
return hash ^ (x + y + hovered)
}, 0)
const highlightHash = (highlightDocumentIds ?? []).reduce((hash, id) => {
return hash ^ id.length
}, 0)
return (
positionHash ^
edges.length ^
Math.round(panX) ^
Math.round(panY) ^
Math.round(zoom * 100) ^
width ^
height ^
highlightHash
)
}, [nodes, edges.length, panX, panY, zoom, width, height, highlightDocumentIds])
useEffect(() => {
if (renderKey !== lastRenderParams.current) {
lastRenderParams.current = renderKey
render()
}
}, [renderKey, render])
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const handleNativeWheel = (e: WheelEvent) => {
e.preventDefault()
e.stopPropagation()
onWheel({
deltaY: e.deltaY,
deltaX: e.deltaX,
clientX: e.clientX,
clientY: e.clientY,
currentTarget: canvas,
nativeEvent: e,
preventDefault: () => {},
stopPropagation: () => {},
} as unknown as React.WheelEvent)
}
canvas.addEventListener("wheel", handleNativeWheel, { passive: false })
const handleGesture = (e: Event) => {
e.preventDefault()
}
canvas.addEventListener("gesturestart", handleGesture, { passive: false })
canvas.addEventListener("gesturechange", handleGesture, { passive: false })
canvas.addEventListener("gestureend", handleGesture, { passive: false })
return () => {
canvas.removeEventListener("wheel", handleNativeWheel)
canvas.removeEventListener("gesturestart", handleGesture)
canvas.removeEventListener("gesturechange", handleGesture)
canvas.removeEventListener("gestureend", handleGesture)
}
}, [onWheel])
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1
useLayoutEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const MAX_CANVAS_SIZE = 16384
const maxDpr =
width > 0 && height > 0
? Math.min(MAX_CANVAS_SIZE / width, MAX_CANVAS_SIZE / height, dpr)
: dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
canvas.width = Math.min(width * maxDpr, MAX_CANVAS_SIZE)
canvas.height = Math.min(height * maxDpr, MAX_CANVAS_SIZE)
const ctx = canvas.getContext("2d")
ctx?.scale(maxDpr, maxDpr)
}, [width, height, dpr])
return (
<canvas
ref={canvasRef}
className="absolute inset-0 z-0"
onClick={handleClick}
onDoubleClick={onDoubleClick}
onMouseDown={handleMouseDown}
onMouseLeave={onPanEnd}
onMouseMove={(e) => {
handleMouseMove(e)
onPanMove(e)
}}
onMouseUp={onPanEnd}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
style={{
cursor: currentHoveredNode.current ? "pointer" : "move",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
}}
/>
)
},
)
GraphCanvas.displayName = "GraphCanvas"

View file

@ -0,0 +1,443 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { GraphCanvas } from "./graph-canvas"
import { useGraphData } from "./use-graph-data"
import { useGraphInteractions } from "./use-graph-interactions"
import { Legend } from "./legend"
import { LoadingIndicator } from "./loading-indicator"
import { NavigationControls } from "./navigation-controls"
import { NodePopover } from "./node-popover"
import { useViewportGraph } from "@/hooks/use-viewport-graph"
import { useTimelineStream } from "@/hooks/use-timeline-stream"
import type { GraphProps } from "./types"
import type { ViewportBounds, ViewportGraphNode, ViewportGraphEdge } from "@/lib/viewport-graph-types"
export function Graph({ containerTags, children }: GraphProps) {
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
const containerRef = useRef<HTMLDivElement>(null)
const hasAutoFittedRef = useRef(false)
const lastFetchedViewportRef = useRef<string>("")
const [isTimelineMode, setIsTimelineMode] = useState(false)
const [timelineNodes, setTimelineNodes] = useState<ViewportGraphNode[]>([])
const [timelineEdges, setTimelineEdges] = useState<ViewportGraphEdge[]>([])
const pendingFitRef = useRef(false)
const {
nodes: viewportNodes,
edges: viewportEdges,
isLoading,
error,
fetchViewport,
totalLoaded,
} = useViewportGraph({ containerTags, limit: 200, enabled: !isTimelineMode })
const activeNodes = isTimelineMode ? timelineNodes : viewportNodes
const activeEdges = isTimelineMode ? timelineEdges : viewportEdges
const { nodes, edges } = useGraphData(activeNodes, activeEdges)
const {
panX,
panY,
zoom,
selectedNode,
handlePanStart,
handlePanMove,
handlePanEnd,
handleWheel,
handleNodeHover,
handleNodeClick,
handleDoubleClick,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
zoomIn,
zoomOut,
autoFitToViewport,
centerViewportOn,
setSelectedNode,
animateToViewState,
isUserInteracting,
} = useGraphInteractions()
const getWorldViewportBounds = useCallback(() => {
const { width, height } = containerSize
if (width === 0 || height === 0) {
return { minX: -1000, maxX: 1000, minY: -1000, maxY: 1000 }
}
const minX = (0 - panX) / zoom
const maxX = (width - panX) / zoom
const minY = (0 - panY) / zoom
const maxY = (height - panY) / zoom
return { minX, maxX, minY, maxY }
}, [containerSize, panX, panY, zoom])
const computeContentBounds = useCallback((nodeList: typeof nodes) => {
const docNodes = nodeList.filter((n) => n.type === "document")
const targetNodes = docNodes.length > 0 ? docNodes : nodeList
if (targetNodes.length === 0) return null
let minX = Number.POSITIVE_INFINITY
let maxX = Number.NEGATIVE_INFINITY
let minY = Number.POSITIVE_INFINITY
let maxY = Number.NEGATIVE_INFINITY
for (const node of targetNodes) {
if (node.x < minX) minX = node.x
if (node.x > maxX) maxX = node.x
if (node.y < minY) minY = node.y
if (node.y > maxY) maxY = node.y
}
return { minX, maxX, minY, maxY }
}, [])
const handleTimelineBatch = useCallback(
(batchNodes: ViewportGraphNode[], batchEdges: ViewportGraphEdge[]) => {
setTimelineNodes((prev) => {
const existingIds = new Set(prev.map((n) => n.id))
const newNodes = batchNodes.filter((n) => !existingIds.has(n.id))
return [...prev, ...newNodes]
})
setTimelineEdges((prev) => {
const existingIds = new Set(prev.map((e) => e.id))
const newEdges = batchEdges.filter((e) => !existingIds.has(e.id))
return [...prev, ...newEdges]
})
pendingFitRef.current = true
},
[],
)
const handleTimelineComplete = useCallback(
(totalDocuments: number, _totalEdges: number) => {
console.log("[timeline] Complete:", totalDocuments, "documents")
},
[],
)
const {
isStreaming: isTimelineStreaming,
progress: timelineProgress,
startStream: startTimelineStream,
stopStream: stopTimelineStream,
} = useTimelineStream({
containerTags,
batchSize: 2,
delayBetweenBatches: 3000,
onBatch: handleTimelineBatch,
onComplete: handleTimelineComplete,
})
const handleStartTimeline = useCallback(() => {
setIsTimelineMode(true)
setTimelineNodes([])
setTimelineEdges([])
hasAutoFittedRef.current = false
startTimelineStream()
}, [startTimelineStream])
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
const newWidth = containerRef.current.clientWidth
const newHeight = containerRef.current.clientHeight
if (
newWidth !== containerSize.width ||
newHeight !== containerSize.height
) {
setContainerSize({ width: newWidth, height: newHeight })
}
}
}
updateSize()
window.addEventListener("resize", updateSize)
const resizeObserver = new ResizeObserver(updateSize)
if (containerRef.current) {
resizeObserver.observe(containerRef.current)
}
return () => {
window.removeEventListener("resize", updateSize)
resizeObserver.disconnect()
}
}, [containerSize.width, containerSize.height])
const getViewportBounds = useCallback((): ViewportBounds => {
const { width, height } = containerSize
if (width === 0 || height === 0) {
return { minX: -100, maxX: 100, minY: -100, maxY: 100 }
}
const COORDINATE_SCALE = 15
const minX = (0 - panX) / zoom / COORDINATE_SCALE
const maxX = (width - panX) / zoom / COORDINATE_SCALE
const minY = (0 - panY) / zoom / COORDINATE_SCALE
const maxY = (height - panY) / zoom / COORDINATE_SCALE
return { minX, maxX, minY, maxY }
}, [containerSize, panX, panY, zoom])
useEffect(() => {
if (containerSize.width === 0 || containerSize.height === 0) return
const bounds = getViewportBounds()
const boundsKey = `${Math.round(bounds.minX)},${Math.round(bounds.maxX)},${Math.round(bounds.minY)},${Math.round(bounds.maxY)}`
if (boundsKey === lastFetchedViewportRef.current) return
const timeoutId = setTimeout(() => {
lastFetchedViewportRef.current = boundsKey
fetchViewport(bounds)
}, 200)
return () => clearTimeout(timeoutId)
}, [containerSize, panX, panY, zoom, getViewportBounds, fetchViewport])
useEffect(() => {
if (
!hasAutoFittedRef.current &&
nodes.length > 0 &&
containerSize.width > 0 &&
containerSize.height > 0
) {
const timer = setTimeout(() => {
autoFitToViewport(nodes, containerSize.width, containerSize.height)
hasAutoFittedRef.current = true
}, 100)
return () => clearTimeout(timer)
}
}, [nodes, containerSize.width, containerSize.height, autoFitToViewport])
useEffect(() => {
if (nodes.length === 0) {
hasAutoFittedRef.current = false
}
}, [nodes.length])
useEffect(() => {
if (
!isTimelineMode ||
!pendingFitRef.current ||
nodes.length === 0 ||
containerSize.width === 0 ||
containerSize.height === 0
) {
return
}
if (isUserInteracting) {
pendingFitRef.current = false
return
}
pendingFitRef.current = false
const content = computeContentBounds(nodes)
if (!content) return
const viewport = getWorldViewportBounds()
const PADDING = 120
const fullyInside =
content.minX >= viewport.minX + PADDING &&
content.maxX <= viewport.maxX - PADDING &&
content.minY >= viewport.minY + PADDING &&
content.maxY <= viewport.maxY - PADDING
if (fullyInside) {
return
}
const { width, height } = containerSize
const contentWidth = content.maxX - content.minX || 1
const contentHeight = content.maxY - content.minY || 1
const contentCenterX = content.minX + contentWidth / 2
const contentCenterY = content.minY + contentHeight / 2
const availableWidth = width - PADDING * 2
const availableHeight = height - PADDING * 2
const zoomToFitWidth = availableWidth / contentWidth
const zoomToFitHeight = availableHeight / contentHeight
const fitZoom = Math.min(zoomToFitWidth, zoomToFitHeight)
const MIN_ZOOM = 0.05
const MAX_ZOOM = 3
const targetZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, Math.min(zoom, fitZoom)))
if (Math.abs(targetZoom - zoom) < 0.01) {
return
}
const currentCenterX = (width / 2 - panX) / zoom
const currentCenterY = (height / 2 - panY) / zoom
const CENTER_LERP = 0.4
const targetCenterX = currentCenterX + (contentCenterX - currentCenterX) * CENTER_LERP
const targetCenterY = currentCenterY + (contentCenterY - currentCenterY) * CENTER_LERP
const targetPanX = width / 2 - targetCenterX * targetZoom
const targetPanY = height / 2 - targetCenterY * targetZoom
animateToViewState(targetPanX, targetPanY, targetZoom, 800)
}, [
isTimelineMode,
nodes,
containerSize,
panX,
panY,
zoom,
getWorldViewportBounds,
computeContentBounds,
animateToViewState,
isUserInteracting,
])
const handleCenter = useCallback(() => {
if (nodes.length === 0) return
const docNodes = nodes.filter((n) => n.type === "document")
const targetNodes = docNodes.length > 0 ? docNodes : nodes
let sumX = 0
let sumY = 0
for (const node of targetNodes) {
sumX += node.x
sumY += node.y
}
const centerX = sumX / targetNodes.length
const centerY = sumY / targetNodes.length
centerViewportOn(
centerX,
centerY,
containerSize.width,
containerSize.height,
)
}, [nodes, centerViewportOn, containerSize])
const handleAutoFit = useCallback(() => {
autoFitToViewport(nodes, containerSize.width, containerSize.height, {
animate: true,
})
}, [nodes, containerSize, autoFitToViewport])
const selectedNodeData = useMemo(() => {
if (!selectedNode) return null
return nodes.find((n) => n.id === selectedNode) || null
}, [selectedNode, nodes])
const popoverPosition = useMemo(() => {
if (!selectedNodeData) return null
const screenX = selectedNodeData.x * zoom + panX
const screenY = selectedNodeData.y * zoom + panY
const nodeSize = selectedNodeData.size * zoom
let popoverX = screenX + nodeSize / 2 + 20
let popoverY = screenY - 100
const popoverWidth = 320
const popoverHeight = 300
if (popoverX + popoverWidth > containerSize.width) {
popoverX = screenX - nodeSize / 2 - popoverWidth - 20
}
if (popoverY < 0) {
popoverY = 10
}
if (popoverY + popoverHeight > containerSize.height) {
popoverY = containerSize.height - popoverHeight - 10
}
return { x: popoverX, y: popoverY }
}, [selectedNodeData, zoom, panX, panY, containerSize])
if (error) {
return (
<div className="h-full flex items-center justify-center">
<div className="bg-white/5 backdrop-blur-xl border border-white/20 rounded-xl p-6">
<div className="text-red-400">
Error loading documents: {error.message}
</div>
</div>
</div>
)
}
return (
<div className="relative h-full w-full overflow-hidden" ref={containerRef}>
<LoadingIndicator
isLoading={isLoading}
isLoadingMore={false}
totalLoaded={totalLoaded}
/>
<Legend edges={edges} isLoading={isLoading} nodes={nodes} />
{selectedNodeData && popoverPosition && (
<NodePopover
node={selectedNodeData}
x={popoverPosition.x}
y={popoverPosition.y}
onClose={() => setSelectedNode(null)}
containerBounds={containerRef.current?.getBoundingClientRect()}
/>
)}
{!isLoading && nodes.filter((n) => n.type === "document").length === 0 && (
<>{children}</>
)}
{containerSize.width > 0 && containerSize.height > 0 && (
<GraphCanvas
edges={edges}
height={containerSize.height}
nodes={nodes}
highlightDocumentIds={[]}
onDoubleClick={handleDoubleClick}
onNodeClick={handleNodeClick}
onNodeHover={handleNodeHover}
onPanEnd={handlePanEnd}
onPanMove={handlePanMove}
onPanStart={handlePanStart}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onWheel={handleWheel}
panX={panX}
panY={panY}
width={containerSize.width}
zoom={zoom}
selectedNodeId={selectedNode}
/>
)}
{containerSize.width > 0 && (
<NavigationControls
onCenter={handleCenter}
onZoomIn={() =>
zoomIn(containerSize.width / 2, containerSize.height / 2)
}
onZoomOut={() =>
zoomOut(containerSize.width / 2, containerSize.height / 2)
}
onAutoFit={handleAutoFit}
onTimeline={handleStartTimeline}
isTimelineActive={isTimelineStreaming}
timelineProgress={timelineProgress}
nodes={nodes}
/>
)}
</div>
)
}

View file

@ -0,0 +1,10 @@
export { Graph } from "./graph"
export { GraphCanvas } from "./graph-canvas"
export { Legend } from "./legend"
export { LoadingIndicator } from "./loading-indicator"
export { NavigationControls } from "./navigation-controls"
export { NodePopover } from "./node-popover"
export { useGraphData } from "./use-graph-data"
export { useGraphInteractions } from "./use-graph-interactions"
export * from "./types"
export * from "./constants"

View file

@ -0,0 +1,178 @@
"use client"
import { Brain, ChevronDown, ChevronUp, FileText } from "lucide-react"
import { memo, useEffect, useState } from "react"
import { colors } from "./constants"
import type { LegendProps } from "./types"
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=/`
}
const getCookie = (name: string): string | null => {
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)
}
return null
}
export const Legend = memo(function Legend({
id,
nodes = [],
edges = [],
isLoading = false,
}: LegendProps) {
const [isExpanded, setIsExpanded] = useState(true)
const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => {
if (!isInitialized) {
const savedState = getCookie("legendCollapsed")
if (savedState === "true") {
setIsExpanded(false)
} else if (savedState === "false") {
setIsExpanded(true)
}
setIsInitialized(true)
}
}, [isInitialized])
const handleToggleExpanded = () => {
const newExpanded = !isExpanded
setIsExpanded(newExpanded)
setCookie("legendCollapsed", newExpanded ? "false" : "true")
}
const memoryCount = nodes.filter((n) => n.type === "memory").length
const documentCount = nodes.filter((n) => n.type === "document").length
return (
<div
id={id}
className="absolute bottom-4 right-4 z-10 bg-white/5 backdrop-blur-xl border border-white/20 rounded-xl overflow-hidden"
>
<div className="p-3">
{!isExpanded && (
<button
type="button"
onClick={handleToggleExpanded}
className="flex items-center gap-2 text-white/70 hover:text-white transition-colors"
>
<span className="text-sm font-medium">?</span>
<ChevronUp className="w-4 h-4" />
</button>
)}
{isExpanded && (
<>
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-white">Legend</span>
<button
type="button"
onClick={handleToggleExpanded}
className="text-white/60 hover:text-white transition-colors"
>
<ChevronDown className="w-4 h-4" />
</button>
</div>
<div className="space-y-4">
{!isLoading && (
<div>
<div className="text-xs text-white/50 mb-2">Statistics</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<Brain className="w-3.5 h-3.5 text-blue-400" />
<span className="text-xs text-white/70">
{memoryCount} memories
</span>
</div>
<div className="flex items-center gap-2">
<FileText className="w-3.5 h-3.5 text-slate-300" />
<span className="text-xs text-white/70">
{documentCount} documents
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3.5 h-3.5 rounded-full bg-gradient-to-r from-blue-400 to-purple-400" />
<span className="text-xs text-white/70">
{edges.length} connections
</span>
</div>
</div>
</div>
)}
<div>
<div className="text-xs text-white/50 mb-2">Nodes</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<div className="w-4 h-3 rounded bg-white/20 border border-white/40" />
<span className="text-xs text-white/70">Document</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-3.5 h-3.5"
style={{
clipPath:
"polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
backgroundColor: "rgba(147, 196, 253, 0.4)",
border: "1px solid rgba(147, 196, 253, 0.6)",
}}
/>
<span className="text-xs text-white/70">Memory</span>
</div>
</div>
</div>
<div>
<div className="text-xs text-white/50 mb-2">Connections</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-slate-400/40" />
<span className="text-xs text-white/70">Doc Memory</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-0.5"
style={{
background:
"repeating-linear-gradient(90deg, rgba(35, 189, 255, 0.6) 0px, rgba(35, 189, 255, 0.6) 3px, transparent 3px, transparent 6px)",
}}
/>
<span className="text-xs text-white/70">Doc similarity</span>
</div>
</div>
</div>
<div>
<div className="text-xs text-white/50 mb-2">Similarity</div>
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<div className="w-4 h-0.5 bg-cyan-500/30" />
<span className="text-xs text-white/70">Weak</span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-1 bg-cyan-400/90" />
<span className="text-xs text-white/70">Strong</span>
</div>
</div>
</div>
</div>
</>
)}
</div>
</div>
)
})
Legend.displayName = "Legend"

View file

@ -0,0 +1,28 @@
"use client"
import { Sparkles } from "lucide-react"
import { memo } from "react"
import type { LoadingIndicatorProps } from "./types"
export const LoadingIndicator = memo<LoadingIndicatorProps>(
({ isLoading, isLoadingMore, totalLoaded }) => {
if (!isLoading && !isLoadingMore) return null
return (
<div className="absolute top-20 right-4 z-10 bg-white/5 backdrop-blur-xl border border-white/20 rounded-xl overflow-hidden">
<div className="p-3">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-orange-400 animate-pulse" />
<span className="text-sm text-white/70">
{isLoading
? "Loading memory graph..."
: `Loading more documents... (${totalLoaded})`}
</span>
</div>
</div>
</div>
)
},
)
LoadingIndicator.displayName = "LoadingIndicator"

View file

@ -0,0 +1,87 @@
"use client"
import { memo } from "react"
import type { NavigationControlsProps } from "./types"
export const NavigationControls = memo<NavigationControlsProps>(
({
onCenter,
onZoomIn,
onZoomOut,
onAutoFit,
onTimeline,
isTimelineActive,
timelineProgress,
nodes,
className = "",
}) => {
return (
<div
className={`absolute bottom-4 left-4 z-10 flex items-center gap-2 ${className}`}
>
{onTimeline && (
<button
type="button"
onClick={onTimeline}
disabled={isTimelineActive}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors flex items-center gap-2 ${
isTimelineActive
? "bg-blue-500/20 text-blue-400 border border-blue-500/40"
: "text-white/80 bg-white/5 backdrop-blur-xl border border-white/20 hover:bg-white/10 hover:text-white"
}`}
title="Play timeline animation"
>
{isTimelineActive ? (
<>
<span className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
{timelineProgress?.streamed ?? 0}
</>
) : (
"Timeline"
)}
</button>
)}
{nodes.length > 0 && (
<>
<button
type="button"
onClick={onAutoFit}
className="px-3 py-1.5 text-sm font-medium text-white/80 bg-white/5 backdrop-blur-xl border border-white/20 rounded-lg hover:bg-white/10 hover:text-white transition-colors"
title="Auto-fit graph to viewport"
>
Fit
</button>
<button
type="button"
onClick={onCenter}
className="px-3 py-1.5 text-sm font-medium text-white/80 bg-white/5 backdrop-blur-xl border border-white/20 rounded-lg hover:bg-white/10 hover:text-white transition-colors"
title="Center view on graph"
>
Center
</button>
<div className="flex items-center bg-white/5 backdrop-blur-xl border border-white/20 rounded-lg overflow-hidden">
<button
type="button"
onClick={onZoomIn}
className="px-3 py-1.5 text-sm font-medium text-white/80 hover:bg-white/10 hover:text-white transition-colors border-r border-white/20"
title="Zoom in"
>
+
</button>
<button
type="button"
onClick={onZoomOut}
className="px-3 py-1.5 text-sm font-medium text-white/80 hover:bg-white/10 hover:text-white transition-colors"
title="Zoom out"
>
</button>
</div>
</>
)}
</div>
)
},
)
NavigationControls.displayName = "NavigationControls"

View file

@ -0,0 +1,248 @@
"use client"
import { memo, useEffect } from "react"
import type { NodePopoverProps } from "./types"
import type { ViewportDocument, ViewportMemoryEntry } from "@/lib/viewport-graph-types"
export const NodePopover = memo<NodePopoverProps>(function NodePopover({
node,
x,
y,
onClose,
containerBounds,
}) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [onClose])
const backdropStyle = containerBounds
? {
left: `${containerBounds.left}px`,
top: `${containerBounds.top}px`,
width: `${containerBounds.width}px`,
height: `${containerBounds.height}px`,
}
: undefined
return (
<>
<div
onClick={onClose}
className={`fixed z-20 ${containerBounds ? "" : "inset-0"}`}
style={backdropStyle}
/>
<div
onClick={(e) => e.stopPropagation()}
className="fixed z-30 w-80 bg-slate-900/95 backdrop-blur-xl border border-white/20 rounded-xl shadow-2xl overflow-hidden"
style={{
left: `${x}px`,
top: `${y}px`,
}}
>
{node.type === "document" ? (
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-slate-400"
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<h3 className="text-sm font-medium text-white">Document</h3>
</div>
<button
type="button"
onClick={onClose}
className="text-white/40 hover:text-white transition-colors text-xl leading-none"
>
×
</button>
</div>
<div className="space-y-3">
<div>
<div className="text-xs text-white/50 mb-1">Title</div>
<p className="text-sm text-white/90">
{(node.data as ViewportDocument).title || "Untitled Document"}
</p>
</div>
{(node.data as ViewportDocument).summary && (
<div>
<div className="text-xs text-white/50 mb-1">Summary</div>
<p className="text-sm text-white/70 line-clamp-2">
{(node.data as ViewportDocument).summary}
</p>
</div>
)}
<div>
<div className="text-xs text-white/50 mb-1">Type</div>
<p className="text-sm text-white/70">
{(node.data as ViewportDocument).type || "Document"}
</p>
</div>
<div>
<div className="text-xs text-white/50 mb-1">Memory Count</div>
<p className="text-sm text-white/70">
{(node.data as ViewportDocument).memoryEntries?.length || 0}{" "}
memories
</p>
</div>
{(node.data as ViewportDocument).url && (
<div>
<div className="text-xs text-white/50 mb-1">URL</div>
<a
href={(node.data as ViewportDocument).url || undefined}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-400 hover:text-blue-300 flex items-center gap-1"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
View Document
</a>
</div>
)}
<div className="flex items-center justify-between pt-2 border-t border-white/10 text-xs text-white/40">
<div className="flex items-center gap-1">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<span>
{new Date(
(node.data as ViewportDocument).createdAt,
).toLocaleDateString()}
</span>
</div>
<span className="font-mono text-[10px] truncate max-w-[100px]">
{node.id}
</span>
</div>
</div>
</div>
) : (
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-blue-400"
>
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
</svg>
<h3 className="text-sm font-medium text-white">Memory</h3>
</div>
<button
type="button"
onClick={onClose}
className="text-white/40 hover:text-white transition-colors text-xl leading-none"
>
×
</button>
</div>
<div className="space-y-3">
<div>
<div className="text-xs text-white/50 mb-1">Memory</div>
<p className="text-sm text-white/90">
{(node.data as ViewportMemoryEntry).content || "No content"}
</p>
</div>
<div>
<div className="text-xs text-white/50 mb-1">Space</div>
<p className="text-sm text-white/70">
{(node.data as ViewportMemoryEntry).spaceId || "Default"}
</p>
</div>
<div className="flex items-center justify-between pt-2 border-t border-white/10 text-xs text-white/40">
<div className="flex items-center gap-1">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<span>
{new Date(
(node.data as ViewportMemoryEntry).createdAt,
).toLocaleDateString()}
</span>
</div>
<span className="font-mono text-[10px] truncate max-w-[100px]">
{node.id}
</span>
</div>
</div>
</div>
)}
</div>
</>
)
})

View file

@ -0,0 +1,65 @@
export const cosineSimilarity = (
vectorA: number[],
vectorB: number[],
): number => {
if (vectorA.length !== vectorB.length) {
throw new Error("Vectors must have the same length")
}
let dotProduct = 0
for (let i = 0; i < vectorA.length; i++) {
const vectorAi = vectorA[i]
const vectorBi = vectorB[i]
if (
typeof vectorAi !== "number" ||
typeof vectorBi !== "number" ||
Number.isNaN(vectorAi) ||
Number.isNaN(vectorBi)
) {
throw new Error("Vectors must contain only numbers")
}
dotProduct += vectorAi * vectorBi
}
return dotProduct
}
export const calculateSemanticSimilarity = (
document1Embedding: number[] | null,
document2Embedding: number[] | null,
): number => {
if (
document1Embedding &&
document2Embedding &&
document1Embedding.length > 0 &&
document2Embedding.length > 0
) {
const similarity = cosineSimilarity(document1Embedding, document2Embedding)
return similarity >= 0 ? similarity : 0
}
return 0
}
export const getConnectionVisualProps = (similarity: number) => {
const normalizedSimilarity = Math.max(0, Math.min(1, similarity))
return {
opacity: Math.max(0, normalizedSimilarity),
thickness: Math.max(1, normalizedSimilarity * 4),
glow: normalizedSimilarity * 0.6,
pulseDuration: 2000 + (1 - normalizedSimilarity) * 3000,
}
}
export const getMagicalConnectionColor = (
similarity: number,
hue = 220,
): string => {
const normalizedSimilarity = Math.max(0, Math.min(1, similarity))
const saturation = 60 + normalizedSimilarity * 40
const lightness = 40 + normalizedSimilarity * 30
return `hsl(${hue}, ${saturation}%, ${lightness}%)`
}

View file

@ -0,0 +1,96 @@
"use client"
import type {
ViewportDocument,
ViewportMemoryEntry,
ViewportGraphNode,
ViewportGraphEdge,
} from "@/lib/viewport-graph-types"
export type { ViewportDocument, ViewportMemoryEntry, ViewportGraphNode, ViewportGraphEdge }
export type MemoryRelation = "updates" | "extends" | "derives"
export interface GraphNode {
id: string
type: "document" | "memory"
x: number
y: number
data: ViewportDocument | ViewportMemoryEntry
size: number
color: string
isHovered: boolean
parentDocumentId?: string
}
export interface GraphEdge {
id: string
source: string | GraphNode
target: string | GraphNode
similarity: number
color: string
opacity: number
thickness: number
edgeType?: "doc-memory" | "doc-doc" | "version"
relationType?: MemoryRelation
}
export interface GraphCanvasProps {
nodes: GraphNode[]
edges: GraphEdge[]
panX: number
panY: number
zoom: number
width: number
height: number
onNodeHover: (nodeId: string | null) => void
onNodeClick: (nodeId: string) => void
onPanStart: (e: React.MouseEvent) => void
onPanMove: (e: React.MouseEvent) => void
onPanEnd: () => void
onWheel: (e: React.WheelEvent) => void
onDoubleClick: (e: React.MouseEvent) => void
onTouchStart?: (e: React.TouchEvent) => void
onTouchMove?: (e: React.TouchEvent) => void
onTouchEnd?: (e: React.TouchEvent) => void
highlightDocumentIds?: string[]
selectedNodeId?: string | null
}
export interface GraphProps {
containerTags?: string[]
children?: React.ReactNode
}
export interface LegendProps {
nodes?: GraphNode[]
edges?: GraphEdge[]
isLoading?: boolean
id?: string
}
export interface LoadingIndicatorProps {
isLoading: boolean
isLoadingMore: boolean
totalLoaded: number
}
export interface NavigationControlsProps {
onCenter: () => void
onZoomIn: () => void
onZoomOut: () => void
onAutoFit: () => void
onTimeline?: () => void
isTimelineActive?: boolean
timelineProgress?: { streamed: number; total: number | null }
nodes: GraphNode[]
className?: string
}
export interface NodePopoverProps {
node: GraphNode
x: number
y: number
onClose: () => void
containerBounds?: DOMRect
}

View file

@ -0,0 +1,40 @@
"use client"
import { useMemo } from "react"
import type { ViewportGraphNode, ViewportGraphEdge } from "@/lib/viewport-graph-types"
import type { GraphNode, GraphEdge } from "./types"
import { NODE_SIZES } from "./constants"
export function useGraphData(
nodes: ViewportGraphNode[],
edges: ViewportGraphEdge[],
): { nodes: GraphNode[]; edges: GraphEdge[] } {
const graphNodes = useMemo((): GraphNode[] => {
return nodes.map((node) => ({
id: node.id,
type: node.type,
x: node.x,
y: node.y,
data: node.data,
size: node.type === "document" ? NODE_SIZES.document : NODE_SIZES.memory,
color: node.color,
isHovered: node.isHovered,
parentDocumentId: node.parentDocumentId,
}))
}, [nodes])
const graphEdges = useMemo((): GraphEdge[] => {
return edges.map((edge) => ({
id: edge.id,
source: typeof edge.source === "string" ? edge.source : edge.source.id,
target: typeof edge.target === "string" ? edge.target : edge.target.id,
similarity: edge.similarity,
color: edge.color,
opacity: edge.opacity,
thickness: edge.thickness,
edgeType: edge.edgeType,
}))
}, [edges])
return { nodes: graphNodes, edges: graphEdges }
}

View file

@ -0,0 +1,470 @@
"use client"
import { useCallback, useRef, useState } from "react"
import { GRAPH_SETTINGS } from "./constants"
import type { GraphNode } from "./types"
export function useGraphInteractions() {
const [panX, setPanX] = useState(GRAPH_SETTINGS.initialPanX)
const [panY, setPanY] = useState(GRAPH_SETTINGS.initialPanY)
const [zoom, setZoom] = useState(GRAPH_SETTINGS.initialZoom)
const [isPanning, setIsPanning] = useState(false)
const [panStart, setPanStart] = useState({ x: 0, y: 0 })
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
const [selectedNode, setSelectedNode] = useState<string | null>(null)
const [touchState, setTouchState] = useState<{
touches: { id: number; x: number; y: number }[]
lastDistance: number
lastCenter: { x: number; y: number }
isGesturing: boolean
}>({
touches: [],
lastDistance: 0,
lastCenter: { x: 0, y: 0 },
isGesturing: false,
})
const animationRef = useRef<number | null>(null)
const [isAnimating, setIsAnimating] = useState(false)
const [isUserInteracting, setIsUserInteracting] = useState(false)
const interactionTimeoutRef = useRef<number | null>(null)
const markInteracting = useCallback(() => {
setIsUserInteracting(true)
if (interactionTimeoutRef.current != null) {
window.clearTimeout(interactionTimeoutRef.current)
}
interactionTimeoutRef.current = window.setTimeout(() => {
setIsUserInteracting(false)
}, 600)
}, [])
const animateToViewState = useCallback(
(
targetPanX: number,
targetPanY: number,
targetZoom: number,
duration = 300,
) => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
const startPanX = panX
const startPanY = panY
const startZoom = zoom
const startTime = Date.now()
setIsAnimating(true)
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
const easeOut = 1 - (1 - progress) ** 3
const currentPanX = startPanX + (targetPanX - startPanX) * easeOut
const currentPanY = startPanY + (targetPanY - startPanY) * easeOut
const currentZoom = startZoom + (targetZoom - startZoom) * easeOut
setPanX(currentPanX)
setPanY(currentPanY)
setZoom(currentZoom)
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate)
} else {
setIsAnimating(false)
animationRef.current = null
}
}
animate()
},
[panX, panY, zoom],
)
const handlePanStart = useCallback(
(e: React.MouseEvent) => {
markInteracting()
setIsPanning(true)
setPanStart({ x: e.clientX - panX, y: e.clientY - panY })
},
[panX, panY, markInteracting],
)
const handlePanMove = useCallback(
(e: React.MouseEvent) => {
if (!isPanning) return
markInteracting()
const newPanX = e.clientX - panStart.x
const newPanY = e.clientY - panStart.y
setPanX(newPanX)
setPanY(newPanY)
},
[isPanning, panStart, markInteracting],
)
const handlePanEnd = useCallback(() => {
setIsPanning(false)
}, [])
const handleWheel = useCallback(
(e: React.WheelEvent) => {
e.preventDefault()
e.stopPropagation()
markInteracting()
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
const panDelta = e.deltaX * 0.5
setPanX((prev) => prev - panDelta)
return
}
const delta = e.deltaY > 0 ? 0.97 : 1.03
const newZoom = Math.max(0.05, Math.min(3, zoom * delta))
let mouseX = e.clientX
let mouseY = e.clientY
const target = e.currentTarget
if (target && "getBoundingClientRect" in target) {
const rect = target.getBoundingClientRect()
mouseX = e.clientX - rect.left
mouseY = e.clientY - rect.top
}
const worldX = (mouseX - panX) / zoom
const worldY = (mouseY - panY) / zoom
const newPanX = mouseX - worldX * newZoom
const newPanY = mouseY - worldY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
},
[zoom, panX, panY, markInteracting],
)
const zoomIn = useCallback(
(centerX?: number, centerY?: number) => {
markInteracting()
const zoomFactor = 1.2
const newZoom = Math.min(3, zoom * zoomFactor)
if (centerX !== undefined && centerY !== undefined) {
const worldX = (centerX - panX) / zoom
const worldY = (centerY - panY) / zoom
const newPanX = centerX - worldX * newZoom
const newPanY = centerY - worldY * newZoom
animateToViewState(newPanX, newPanY, newZoom, 200)
} else {
setZoom(newZoom)
}
},
[zoom, panX, panY, animateToViewState, markInteracting],
)
const zoomOut = useCallback(
(centerX?: number, centerY?: number) => {
markInteracting()
const zoomFactor = 0.8
const newZoom = Math.max(0.05, zoom * zoomFactor)
if (centerX !== undefined && centerY !== undefined) {
const worldX = (centerX - panX) / zoom
const worldY = (centerY - panY) / zoom
const newPanX = centerX - worldX * newZoom
const newPanY = centerY - worldY * newZoom
animateToViewState(newPanX, newPanY, newZoom, 200)
} else {
setZoom(newZoom)
}
},
[zoom, panX, panY, animateToViewState, markInteracting],
)
const resetView = useCallback(() => {
animateToViewState(
GRAPH_SETTINGS.initialPanX,
GRAPH_SETTINGS.initialPanY,
GRAPH_SETTINGS.initialZoom,
300,
)
}, [animateToViewState])
const autoFitToViewport = useCallback(
(
nodes: GraphNode[],
viewportWidth: number,
viewportHeight: number,
options?: { occludedRightPx?: number; animate?: boolean },
) => {
if (nodes.length === 0) return
const docNodes = nodes.filter((n) => n.type === "document")
const targetNodes = docNodes.length > 0 ? docNodes : nodes
let minX = Number.POSITIVE_INFINITY
let maxX = Number.NEGATIVE_INFINITY
let minY = Number.POSITIVE_INFINITY
let maxY = Number.NEGATIVE_INFINITY
for (const node of targetNodes) {
minX = Math.min(minX, node.x)
maxX = Math.max(maxX, node.x)
minY = Math.min(minY, node.y)
maxY = Math.max(maxY, node.y)
}
const contentWidth = maxX - minX
const contentHeight = maxY - minY
const contentCenterX = minX + contentWidth / 2
const contentCenterY = minY + contentHeight / 2
const paddingX = 100
const paddingY = 100
const occludedRightPx = options?.occludedRightPx ?? 0
const availableWidth = viewportWidth - occludedRightPx - paddingX * 2
const availableHeight = viewportHeight - paddingY * 2
const zoomToFitWidth =
contentWidth > 0 ? availableWidth / contentWidth : 1
const zoomToFitHeight =
contentHeight > 0 ? availableHeight / contentHeight : 1
const newZoom = Math.min(
Math.max(0.1, Math.min(zoomToFitWidth, zoomToFitHeight)),
1.5,
)
const availableCenterX = availableWidth / 2
const newPanX = availableCenterX - contentCenterX * newZoom
const newPanY = viewportHeight / 2 - contentCenterY * newZoom
if (options?.animate) {
const steps = 8
const durationMs = 160
const intervalMs = Math.max(1, Math.floor(durationMs / steps))
const startZoom = zoom
const startPanX = panX
const startPanY = panY
let i = 0
const ease = (t: number) => 1 - (1 - t) ** 2
const timer = setInterval(() => {
i++
const t = ease(i / steps)
setZoom(startZoom + (newZoom - startZoom) * t)
setPanX(startPanX + (newPanX - startPanX) * t)
setPanY(startPanY + (newPanY - startPanY) * t)
if (i >= steps) clearInterval(timer)
}, intervalMs)
} else {
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
}
},
[zoom, panX, panY],
)
const handleTouchStart = useCallback((e: React.TouchEvent) => {
markInteracting()
const touches = Array.from(e.touches).map((touch) => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY,
}))
if (touches.length >= 2) {
const touch1 = touches[0]!
const touch2 = touches[1]!
const distance = Math.sqrt(
(touch2.x - touch1.x) ** 2 + (touch2.y - touch1.y) ** 2,
)
const center = {
x: (touch1.x + touch2.x) / 2,
y: (touch1.y + touch2.y) / 2,
}
setTouchState({
touches,
lastDistance: distance,
lastCenter: center,
isGesturing: true,
})
} else {
setTouchState((prev) => ({ ...prev, touches, isGesturing: false }))
}
}, [markInteracting])
const handleTouchMove = useCallback(
(e: React.TouchEvent) => {
e.preventDefault()
markInteracting()
const touches = Array.from(e.touches).map((touch) => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY,
}))
if (touches.length >= 2 && touchState.isGesturing) {
const touch1 = touches[0]!
const touch2 = touches[1]!
const distance = Math.sqrt(
(touch2.x - touch1.x) ** 2 + (touch2.y - touch1.y) ** 2,
)
const center = {
x: (touch1.x + touch2.x) / 2,
y: (touch1.y + touch2.y) / 2,
}
const distanceChange = distance / touchState.lastDistance
const newZoom = Math.max(0.05, Math.min(3, zoom * distanceChange))
const canvas = e.currentTarget as HTMLElement
const rect = canvas.getBoundingClientRect()
const centerX = center.x - rect.left
const centerY = center.y - rect.top
const worldX = (centerX - panX) / zoom
const worldY = (centerY - panY) / zoom
const newPanX = centerX - worldX * newZoom
const newPanY = centerY - worldY * newZoom
const centerDx = center.x - touchState.lastCenter.x
const centerDy = center.y - touchState.lastCenter.y
setZoom(newZoom)
setPanX(newPanX + centerDx)
setPanY(newPanY + centerDy)
setTouchState({
touches,
lastDistance: distance,
lastCenter: center,
isGesturing: true,
})
} else if (touches.length === 1 && !touchState.isGesturing && isPanning) {
const touch = touches[0]!
const newPanX = touch.x - panStart.x
const newPanY = touch.y - panStart.y
setPanX(newPanX)
setPanY(newPanY)
}
},
[touchState, zoom, panX, panY, isPanning, panStart, markInteracting],
)
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
const touches = Array.from(e.touches).map((touch) => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY,
}))
if (touches.length < 2) {
setTouchState((prev) => ({ ...prev, touches, isGesturing: false }))
} else {
setTouchState((prev) => ({ ...prev, touches }))
}
if (touches.length === 0) {
setIsPanning(false)
}
}, [])
const centerViewportOn = useCallback(
(
worldX: number,
worldY: number,
viewportWidth: number,
viewportHeight: number,
animate = true,
) => {
const newPanX = viewportWidth / 2 - worldX * zoom
const newPanY = viewportHeight / 2 - worldY * zoom
if (animate && !isAnimating) {
animateToViewState(newPanX, newPanY, zoom, 400)
} else {
setPanX(newPanX)
setPanY(newPanY)
}
},
[zoom, isAnimating, animateToViewState],
)
const handleNodeHover = useCallback((nodeId: string | null) => {
setHoveredNode(nodeId)
}, [])
const handleNodeClick = useCallback(
(nodeId: string) => {
setSelectedNode(selectedNode === nodeId ? null : nodeId)
},
[selectedNode],
)
const handleDoubleClick = useCallback(
(e: React.MouseEvent) => {
markInteracting()
const zoomFactor = 1.5
const newZoom = Math.min(3, zoom * zoomFactor)
let mouseX = e.clientX
let mouseY = e.clientY
const target = e.currentTarget
if (target && "getBoundingClientRect" in target) {
const rect = target.getBoundingClientRect()
mouseX = e.clientX - rect.left
mouseY = e.clientY - rect.top
}
const worldX = (mouseX - panX) / zoom
const worldY = (mouseY - panY) / zoom
const newPanX = mouseX - worldX * newZoom
const newPanY = mouseY - worldY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
},
[zoom, panX, panY, markInteracting],
)
return {
panX,
panY,
zoom,
hoveredNode,
selectedNode,
handlePanStart,
handlePanMove,
handlePanEnd,
handleWheel,
handleNodeHover,
handleNodeClick,
handleDoubleClick,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
zoomIn,
zoomOut,
resetView,
autoFitToViewport,
centerViewportOn,
setSelectedNode,
animateToViewState,
isUserInteracting,
}
}

View file

@ -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>

View file

@ -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>
);
)
}

View file

@ -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

View file

@ -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

View file

@ -13,11 +13,16 @@ export function McpPreview({ document }: { document: DocumentWithMemories }) {
return (
<div className="bg-[#0B1017] p-3 rounded-[18px] space-y-2">
<div className="flex items-center justify-between gap-1">
<p className={cn(dmSansClassName(), "text-[12px] font-semibold flex items-center gap-1")}>
<ClaudeDesktopIcon className="size-3" />
<p
className={cn(
dmSansClassName(),
"text-[12px] font-semibold flex items-center gap-1",
)}
>
<ClaudeDesktopIcon className="size-3" />
Claude Desktop
</p>
<MCPIcon className="size-6" />
<MCPIcon className="size-6" />
</div>
<div className="space-y-[6px]">
{document.title && (

View file

@ -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}

View file

@ -211,7 +211,9 @@ export function GraphListMemories({
/>
</g>
</svg>
<p className="group-hover:text-white group-data-[state=active]:text-white">Graph</p>
<p className="group-hover:text-white group-data-[state=active]:text-white">
Graph
</p>
</TabsTrigger>
<TabsTrigger
value="list"
@ -233,7 +235,9 @@ export function GraphListMemories({
className="fill-[#737373] group-hover:fill-white group-data-[state=active]:fill-white"
/>
</svg>
<p className="group-hover:text-white group-data-[state=active]:text-white">List</p>
<p className="group-hover:text-white group-data-[state=active]:text-white">
List
</p>
</TabsTrigger>
</TabsList>
</Tabs>

View file

@ -59,7 +59,9 @@ export function MCPModal({
Migrate from MCP v1
</Button>
</div>
<Button variant="insideOut" className="px-6 py-[10px]">Done</Button>
<Button variant="insideOut" className="px-6 py-[10px]">
Done
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -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\./, "")
}

View file

@ -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)} />
}

View file

@ -1,6 +1,6 @@
"use client"
import { motion, useReducedMotion, Variants } from "motion/react"
import { motion, useReducedMotion, type Variants } from "motion/react"
type NovaPathLoaderProps = {
size?: number // px

View file

@ -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>
)
}

View file

@ -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)

View file

@ -0,0 +1,73 @@
// Enhanced glass-morphism color palette (from memory-graph)
export const colors = {
background: {
primary: "#0f1419",
secondary: "#1a1f29",
accent: "#252a35",
},
document: {
primary: "rgba(255, 255, 255, 0.21)",
secondary: "rgba(255, 255, 255, 0.31)",
accent: "rgba(255, 255, 255, 0.31)",
border: "rgba(255, 255, 255, 0.6)",
glow: "rgba(147, 197, 253, 0.4)",
},
connection: {
weak: "rgba(35, 189, 255, 0.3)",
medium: "rgba(35, 189, 255, 0.6)",
strong: "rgba(35, 189, 255, 0.9)",
},
text: {
primary: "#ffffff",
secondary: "#e2e8f0",
muted: "#94a3b8",
},
accent: {
primary: "rgba(59, 130, 246, 0.7)",
secondary: "rgba(99, 102, 241, 0.6)",
glow: "rgba(147, 197, 253, 0.6)",
},
}
// Graph view settings
export const GRAPH_SETTINGS = {
console: {
initialZoom: 0.8,
initialPanX: 0,
initialPanY: 0,
},
consumer: {
initialZoom: 0.5,
initialPanX: 400,
initialPanY: 300,
},
}
// Animation settings
export const ANIMATION = {
dimDuration: 1500,
}
// Responsive positioning for different app variants
export const POSITIONING = {
console: {
legend: {
desktop: "bottom-4 right-4",
mobile: "bottom-4 right-4",
},
loadingIndicator: "top-20 right-4",
spacesSelector: "top-4 left-4",
viewToggle: "",
nodeDetail: "top-4 right-4",
},
consumer: {
legend: {
desktop: "top-18 right-4",
mobile: "bottom-[180px] left-4",
},
loadingIndicator: "top-20 right-4",
spacesSelector: "",
viewToggle: "top-4 right-4",
nodeDetail: "top-4 right-4",
},
}

View file

@ -0,0 +1,209 @@
/**
* Canvas-based document type icon rendering utilities
* Simplified to match supported file types: PDF, TXT, MD, DOCX, DOC, RTF, CSV, JSON
*/
export type DocumentIconType =
| "text"
| "pdf"
| "md"
| "markdown"
| "docx"
| "doc"
| "rtf"
| "csv"
| "json"
export function drawDocumentIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
type: string,
color = "rgba(255, 255, 255, 0.9)",
): void {
ctx.save()
ctx.fillStyle = color
ctx.strokeStyle = color
ctx.lineWidth = Math.max(1, size / 12)
ctx.lineCap = "round"
ctx.lineJoin = "round"
switch (type) {
case "pdf":
drawPdfIcon(ctx, x, y, size)
break
case "md":
case "markdown":
drawMarkdownIcon(ctx, x, y, size)
break
case "doc":
case "docx":
drawWordIcon(ctx, x, y, size)
break
case "rtf":
drawRtfIcon(ctx, x, y, size)
break
case "csv":
drawCsvIcon(ctx, x, y, size)
break
case "json":
drawJsonIcon(ctx, x, y, size)
break
case "txt":
case "text":
default:
drawTextIcon(ctx, x, y, size)
break
}
ctx.restore()
}
function drawTextIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
const cornerFold = size * 0.2
ctx.beginPath()
ctx.moveTo(x - w / 2, y - h / 2)
ctx.lineTo(x + w / 2 - cornerFold, y - h / 2)
ctx.lineTo(x + w / 2, y - h / 2 + cornerFold)
ctx.lineTo(x + w / 2, y + h / 2)
ctx.lineTo(x - w / 2, y + h / 2)
ctx.closePath()
ctx.stroke()
const lineSpacing = size * 0.15
const lineWidth = size * 0.4
ctx.beginPath()
ctx.moveTo(x - lineWidth / 2, y - lineSpacing)
ctx.lineTo(x + lineWidth / 2, y - lineSpacing)
ctx.moveTo(x - lineWidth / 2, y)
ctx.lineTo(x + lineWidth / 2, y)
ctx.moveTo(x - lineWidth / 2, y + lineSpacing)
ctx.lineTo(x + lineWidth / 2, y + lineSpacing)
ctx.stroke()
}
function drawPdfIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.35}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("PDF", x, y)
}
function drawMarkdownIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.3}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("MD", x, y)
}
function drawWordIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.28}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("DOC", x, y)
}
function drawRtfIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.beginPath()
ctx.rect(x - w / 2, y - h / 2, w, h)
ctx.stroke()
ctx.font = `bold ${size * 0.3}px sans-serif`
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillText("RTF", x, y)
}
function drawCsvIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.7
const h = size * 0.85
ctx.strokeRect(x - w / 2, y - h / 2, w, h)
ctx.beginPath()
ctx.moveTo(x, y - h / 2)
ctx.lineTo(x, y + h / 2)
ctx.moveTo(x - w / 2, y)
ctx.lineTo(x + w / 2, y)
ctx.stroke()
}
function drawJsonIcon(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
): void {
const w = size * 0.6
const h = size * 0.8
ctx.beginPath()
ctx.moveTo(x - w / 4, y - h / 2)
ctx.quadraticCurveTo(x - w / 2, y - h / 3, x - w / 2, y)
ctx.quadraticCurveTo(x - w / 2, y + h / 3, x - w / 4, y + h / 2)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x + w / 4, y - h / 2)
ctx.quadraticCurveTo(x + w / 2, y - h / 3, x + w / 2, y)
ctx.quadraticCurveTo(x + w / 2, y + h / 3, x + w / 4, y + h / 2)
ctx.stroke()
}

View file

@ -0,0 +1,5 @@
export { ViewportGraph } from "./viewport-graph"
export { ViewportCanvas } from "./viewport-canvas"
export { NodePopover } from "./node-popover"
export { NavigationControls } from "./navigation-controls"
export { LoadingIndicator } from "./loading-indicator"

View file

@ -0,0 +1,28 @@
"use client"
import { memo } from "react"
import { Loader2 } from "lucide-react"
interface LoadingIndicatorProps {
isLoading: boolean
totalLoaded: number
}
export const LoadingIndicator = memo<LoadingIndicatorProps>(
({ isLoading, totalLoaded }) => {
if (!isLoading && totalLoaded === 0) {
return null
}
return (
<div className="absolute top-4 right-4 flex items-center gap-2 px-3 py-1.5 bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg text-xs text-white/80">
{isLoading && <Loader2 className="w-3 h-3 animate-spin" />}
<span>
{totalLoaded} document{totalLoaded !== 1 ? "s" : ""}
</span>
</div>
)
},
)
LoadingIndicator.displayName = "LoadingIndicator"

View file

@ -0,0 +1,65 @@
"use client"
import { memo } from "react"
import type { ViewportGraphNode } from "@/lib/viewport-graph-types"
import { Plus, Minus, Maximize2, Target } from "lucide-react"
interface NavigationControlsProps {
onCenter: () => void
onZoomIn: () => void
onZoomOut: () => void
onAutoFit: () => void
nodes: ViewportGraphNode[]
className?: string
}
export const NavigationControls = memo<NavigationControlsProps>(
({ onCenter, onZoomIn, onZoomOut, onAutoFit, nodes, className = "" }) => {
if (nodes.length === 0) {
return null
}
return (
<div
className={`absolute bottom-4 right-4 flex items-center gap-2 ${className}`}
>
<button
type="button"
onClick={onAutoFit}
className="px-3 py-1.5 text-xs font-medium text-white/80 bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 rounded-lg transition-colors"
title="Auto-fit graph to viewport"
>
<Maximize2 className="w-4 h-4" />
</button>
<button
type="button"
onClick={onCenter}
className="px-3 py-1.5 text-xs font-medium text-white/80 bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 rounded-lg transition-colors"
title="Center view on graph"
>
<Target className="w-4 h-4" />
</button>
<div className="flex items-center bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg overflow-hidden">
<button
type="button"
onClick={onZoomIn}
className="px-2.5 py-1.5 text-white/80 hover:bg-white/10 transition-colors border-r border-white/20"
title="Zoom in"
>
<Plus className="w-4 h-4" />
</button>
<button
type="button"
onClick={onZoomOut}
className="px-2.5 py-1.5 text-white/80 hover:bg-white/10 transition-colors"
title="Zoom out"
>
<Minus className="w-4 h-4" />
</button>
</div>
</div>
)
},
)
NavigationControls.displayName = "NavigationControls"

View file

@ -0,0 +1,187 @@
"use client"
import { memo, useEffect } from "react"
import type { ViewportGraphNode, ViewportDocument, ViewportMemoryEntry } from "@/lib/viewport-graph-types"
import { FileText, Calendar, Hash, ExternalLink, X, Brain } from "lucide-react"
export interface NodePopoverProps {
node: ViewportGraphNode
x: number
y: number
onClose: () => void
containerBounds?: DOMRect
}
export const NodePopover = memo<NodePopoverProps>(function NodePopover({
node,
x,
y,
onClose,
containerBounds,
}) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [onClose])
const backdropStyle = containerBounds
? {
left: `${containerBounds.left}px`,
top: `${containerBounds.top}px`,
width: `${containerBounds.width}px`,
height: `${containerBounds.height}px`,
}
: undefined
const isMemory = node.type === "memory"
const doc = isMemory ? null : (node.data as ViewportDocument)
const memory = isMemory ? (node.data as ViewportMemoryEntry) : null
const getDocumentUrl = () => {
if (!doc) return undefined
if (doc.type === "google_doc" && doc.customId) {
return `https://docs.google.com/document/d/${doc.customId}`
}
if (doc.type === "google_sheet" && doc.customId) {
return `https://docs.google.com/spreadsheets/d/${doc.customId}`
}
if (doc.type === "google_slide" && doc.customId) {
return `https://docs.google.com/presentation/d/${doc.customId}`
}
return doc.url ?? undefined
}
const documentUrl = getDocumentUrl()
const title = doc?.title ?? memory?.title ?? "Untitled"
const summary = doc?.summary ?? memory?.summary ?? memory?.content
const type = doc?.type ?? memory?.type ?? "Memory"
const createdAt = doc?.createdAt ?? memory?.createdAt
const url = doc?.url ?? memory?.url
return (
<>
{/* Backdrop */}
<div
onClick={onClose}
className={`fixed z-[999] ${containerBounds ? "" : "inset-0"}`}
style={backdropStyle}
/>
{/* Popover */}
<div
onClick={(e) => e.stopPropagation()}
className="fixed z-[1000] bg-white/5 backdrop-blur-xl border border-white/25 rounded-xl p-4 w-80 shadow-2xl"
style={{
left: `${x}px`,
top: `${y}px`,
}}
>
<div className="flex flex-col gap-3">
{/* Header */}
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
{isMemory ? (
<Brain className="w-5 h-5 text-purple-400" />
) : (
<FileText className="w-5 h-5 text-slate-400" />
)}
<h3 className="text-base font-bold text-white">
{isMemory ? "Memory" : "Document"}
</h3>
</div>
<button
type="button"
onClick={onClose}
className="p-1 bg-transparent text-slate-400 hover:text-white transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Content */}
<div className="flex flex-col gap-3">
{/* Title */}
<div>
<div className="text-[11px] text-slate-400/80 uppercase tracking-wide mb-1">
Title
</div>
<p className="text-sm text-slate-300 leading-relaxed">
{title || "Untitled"}
</p>
</div>
{/* Summary/Content */}
{summary && (
<div>
<div className="text-[11px] text-slate-400/80 uppercase tracking-wide mb-1">
{isMemory ? "Content" : "Summary"}
</div>
<p className="text-sm text-slate-300 leading-relaxed line-clamp-3">
{summary}
</p>
</div>
)}
{/* Type */}
<div>
<div className="text-[11px] text-slate-400/80 uppercase tracking-wide mb-1">
Type
</div>
<p className="text-sm text-slate-300">{type}</p>
</div>
{/* Memory Count - only for documents */}
{doc && (
<div>
<div className="text-[11px] text-slate-400/80 uppercase tracking-wide mb-1">
Memory Count
</div>
<p className="text-sm text-slate-300">
{doc.memoryEntries?.length || 0} memories
</p>
</div>
)}
{/* URL */}
{(documentUrl || url) && (
<div>
<div className="text-[11px] text-slate-400/80 uppercase tracking-wide mb-1">
URL
</div>
<a
href={documentUrl || url || "#"}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1"
>
<ExternalLink className="w-3 h-3" />
{isMemory ? "View Source" : "View Document"}
</a>
</div>
)}
{/* Footer */}
<div className="pt-3 border-t border-slate-600/50 flex items-center gap-4 text-xs text-slate-400">
{createdAt && (
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>{new Date(createdAt).toLocaleDateString()}</span>
</div>
)}
<div className="flex items-center gap-1 overflow-hidden flex-1">
<Hash className="w-3 h-3 flex-shrink-0" />
<span className="truncate">{node.id}</span>
</div>
</div>
</div>
</div>
</div>
</>
)
})

View file

@ -0,0 +1,675 @@
"use client"
import {
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import type {
ViewportCanvasProps,
ViewportDocument,
ViewportGraphNode,
} from "@/lib/viewport-graph-types"
import { colors, ANIMATION } from "./constants"
import { drawDocumentIcon } from "./document-icons"
export const ViewportCanvas = memo<ViewportCanvasProps>(
({
nodes,
edges,
panX,
panY,
zoom,
width,
height,
onNodeHover,
onNodeClick,
onPanStart,
onPanMove,
onPanEnd,
onWheel,
onDoubleClick,
onTouchStart,
onTouchMove,
onTouchEnd,
highlightDocumentIds,
selectedNodeId = null,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const startTimeRef = useRef<number>(Date.now())
const mousePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const currentHoveredNode = useRef<string | null>(null)
const dimProgress = useRef<number>(selectedNodeId ? 1 : 0)
const dimAnimationRef = useRef<number>(0)
const [, forceRender] = useState(0)
// Initialize start time once
useEffect(() => {
startTimeRef.current = Date.now()
}, [])
// Initialize canvas quality settings once
useLayoutEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = "high"
}, [])
// Smooth dimming animation
useEffect(() => {
const targetDim = selectedNodeId ? 1 : 0
const duration = ANIMATION.dimDuration
const startDim = dimProgress.current
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
const eased = 1 - (1 - progress) ** 3
dimProgress.current = startDim + (targetDim - startDim) * eased
forceRender((prev) => prev + 1)
if (progress < 1) {
dimAnimationRef.current = requestAnimationFrame(animate)
}
}
if (dimAnimationRef.current) {
cancelAnimationFrame(dimAnimationRef.current)
}
animate()
return () => {
if (dimAnimationRef.current) {
cancelAnimationFrame(dimAnimationRef.current)
}
}
}, [selectedNodeId])
// Spatial grid for optimized hit detection
const spatialGrid = useMemo(() => {
const GRID_CELL_SIZE = 150
const grid = new Map<string, ViewportGraphNode[]>()
nodes.forEach((node) => {
const screenX = node.x * zoom + panX
const screenY = node.y * zoom + panY
const cellX = Math.floor(screenX / GRID_CELL_SIZE)
const cellY = Math.floor(screenY / GRID_CELL_SIZE)
const cellKey = `${cellX},${cellY}`
if (!grid.has(cellKey)) {
grid.set(cellKey, [])
}
grid.get(cellKey)!.push(node)
})
return { grid, cellSize: GRID_CELL_SIZE }
}, [nodes, panX, panY, zoom])
// Efficient hit detection using spatial grid
const getNodeAtPosition = useCallback(
(x: number, y: number): string | null => {
const { grid, cellSize } = spatialGrid
const cellX = Math.floor(x / cellSize)
const cellY = Math.floor(y / cellSize)
const cellKey = `${cellX},${cellY}`
const cellsToCheck = [
cellKey,
`${cellX - 1},${cellY}`,
`${cellX + 1},${cellY}`,
`${cellX},${cellY - 1}`,
`${cellX},${cellY + 1}`,
]
for (const key of cellsToCheck) {
const cellNodes = grid.get(key)
if (!cellNodes) continue
for (let i = cellNodes.length - 1; i >= 0; i--) {
const node = cellNodes[i]!
const screenX = node.x * zoom + panX
const screenY = node.y * zoom + panY
const nodeSize = node.size * zoom
// Rectangular hit detection for documents
const docWidth = nodeSize * 1.4
const docHeight = nodeSize * 0.9
const halfW = docWidth / 2
const halfH = docHeight / 2
if (
x >= screenX - halfW &&
x <= screenX + halfW &&
y >= screenY - halfH &&
y <= screenY + halfH
) {
return node.id
}
}
}
return null
},
[spatialGrid, panX, panY, zoom],
)
// Build node map for O(1) lookups
const nodeMap = useMemo(() => {
const map = new Map<string, ViewportGraphNode>()
nodes.forEach((node) => map.set(node.id, node))
return map
}, [nodes])
// Handle mouse events
const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
mousePos.current = { x, y }
const nodeId = getNodeAtPosition(x, y)
if (nodeId !== currentHoveredNode.current) {
currentHoveredNode.current = nodeId
onNodeHover(nodeId)
}
onPanMove(e)
},
[getNodeAtPosition, onNodeHover, onPanMove],
)
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const nodeId = getNodeAtPosition(x, y)
if (nodeId) {
e.stopPropagation()
return
}
onPanStart(e)
},
[getNodeAtPosition, onPanStart],
)
const handleClick = useCallback(
(e: React.MouseEvent) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const nodeId = getNodeAtPosition(x, y)
if (nodeId) {
onNodeClick(nodeId)
}
},
[getNodeAtPosition, onNodeClick],
)
// Main render function
const render = useCallback(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const dpr =
typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1
ctx.save()
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, width, height)
// Level-of-detail optimization based on zoom
const useSimplifiedRendering = zoom < 0.3
// Draw minimal background grid
ctx.strokeStyle = "rgba(148, 163, 184, 0.03)"
ctx.lineWidth = 1
const gridSpacing = 100 * zoom
const offsetX = panX % gridSpacing
const offsetY = panY % gridSpacing
for (let x = offsetX; x < width; x += gridSpacing) {
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, height)
ctx.stroke()
}
for (let y = offsetY; y < height; y += gridSpacing) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(width, y)
ctx.stroke()
}
// Performance: calculate viewport bounds for culling
const viewMinX = -panX / zoom - 200
const viewMaxX = (width - panX) / zoom + 200
const viewMinY = -panY / zoom - 200
const viewMaxY = (height - panY) / zoom + 200
const visibleNodes = nodes.filter(
(n) =>
n.x >= viewMinX &&
n.x <= viewMaxX &&
n.y >= viewMinY &&
n.y <= viewMaxY,
)
const visibleNodeIds = new Set(visibleNodes.map((n) => n.id))
// Smooth edge opacity: interpolate between full and 0.05 (dimmed)
const edgeDimOpacity = 1 - dimProgress.current * 0.95
// Draw edges with batched rendering
ctx.lineCap = "round"
// Filter visible edges
const visibleEdges = edges.filter((edge) => {
const sourceId =
typeof edge.source === "string" ? edge.source : edge.source.id
const targetId =
typeof edge.target === "string" ? edge.target : edge.target.id
return visibleNodeIds.has(sourceId) && visibleNodeIds.has(targetId)
})
// Draw doc-doc edges with dashed lines and similarity-based styling
ctx.setLineDash(useSimplifiedRendering ? [] : [10, 5])
visibleEdges.forEach((edge) => {
const sourceId =
typeof edge.source === "string" ? edge.source : edge.source.id
const targetId =
typeof edge.target === "string" ? edge.target : edge.target.id
const sourceNode = nodeMap.get(sourceId)
const targetNode = nodeMap.get(targetId)
if (!sourceNode || !targetNode) return
const sourceX = sourceNode.x * zoom + panX
const sourceY = sourceNode.y * zoom + panY
const targetX = targetNode.x * zoom + panX
const targetY = targetNode.y * zoom + panY
const edgeShouldDim =
selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim
? edgeDimOpacity
: Math.max(0, edge.similarity * 0.5)
const lineWidth = Math.max(1, edge.similarity * 2)
// Set color based on similarity strength
let connectionColor = colors.connection.weak
if (edge.similarity > 0.85) connectionColor = colors.connection.strong
else if (edge.similarity > 0.725)
connectionColor = colors.connection.medium
ctx.strokeStyle = connectionColor
ctx.lineWidth = lineWidth
ctx.globalAlpha = opacity
if (useSimplifiedRendering) {
// Straight lines for performance
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
ctx.lineTo(targetX, targetY)
ctx.stroke()
} else {
// Curved lines when zoomed in
const midX = (sourceX + targetX) / 2
const midY = (sourceY + targetY) / 2
const dx = targetX - sourceX
const dy = targetY - sourceY
const distance = Math.sqrt(dx * dx + dy * dy)
const controlOffset = Math.min(30, distance * 0.2)
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
ctx.quadraticCurveTo(
midX + controlOffset * (dy / distance),
midY - controlOffset * (dx / distance),
targetX,
targetY,
)
ctx.stroke()
}
})
ctx.globalAlpha = 1
ctx.setLineDash([])
// Prepare highlight set from provided document IDs
const highlightSet = new Set<string>(highlightDocumentIds ?? [])
// Draw nodes with enhanced styling
visibleNodes.forEach((node) => {
const screenX = node.x * zoom + panX
const screenY = node.y * zoom + panY
const nodeSize = node.size * zoom
const isHovered = currentHoveredNode.current === node.id
const isDragging = node.isDragging
const isSelected = selectedNodeId === node.id
const shouldDim = selectedNodeId !== null && !isSelected
const nodeOpacity = shouldDim ? 1 - dimProgress.current * 0.9 : 1
const isHighlightedDocument = (() => {
if (highlightSet.size === 0) return false
const doc = node.data as ViewportDocument
if (doc.customId && highlightSet.has(doc.customId)) return true
return highlightSet.has(doc.id)
})()
// Draw memory nodes as circles
if (node.type === "memory") {
const radius = nodeSize / 2
ctx.globalAlpha = nodeOpacity
ctx.fillStyle = isHovered
? "rgba(167, 139, 250, 0.4)"
: "rgba(167, 139, 250, 0.25)"
ctx.strokeStyle = isHovered
? "rgba(167, 139, 250, 0.9)"
: "rgba(167, 139, 250, 0.6)"
ctx.lineWidth = isHovered ? 2 : 1
ctx.beginPath()
ctx.arc(screenX, screenY, radius, 0, Math.PI * 2)
ctx.fill()
ctx.stroke()
// Draw memory icon (small dot pattern)
if (!useSimplifiedRendering && nodeSize > 20) {
ctx.fillStyle = "rgba(255, 255, 255, 0.6)"
const dotSize = radius * 0.15
ctx.beginPath()
ctx.arc(screenX, screenY - dotSize * 2, dotSize, 0, Math.PI * 2)
ctx.arc(screenX - dotSize * 1.5, screenY + dotSize, dotSize, 0, Math.PI * 2)
ctx.arc(screenX + dotSize * 1.5, screenY + dotSize, dotSize, 0, Math.PI * 2)
ctx.fill()
}
ctx.globalAlpha = 1
return // Skip document rendering for memory nodes
}
// Enhanced glassmorphism document styling
const docWidth = nodeSize * 1.4
const docHeight = nodeSize * 0.9
// Multi-layer glass effect
ctx.fillStyle = isDragging
? colors.document.accent
: isHovered
? colors.document.secondary
: colors.document.primary
ctx.globalAlpha = nodeOpacity
// Enhanced border with subtle glow
ctx.strokeStyle = isDragging
? colors.document.glow
: isHovered
? colors.document.accent
: colors.document.border
ctx.lineWidth = isDragging ? 3 : isHovered ? 2 : 1
// Rounded rectangle with enhanced styling
const radius = useSimplifiedRendering ? 6 : 12
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2,
screenY - docHeight / 2,
docWidth,
docHeight,
radius,
)
ctx.fill()
ctx.stroke()
// Subtle inner highlight for glass effect (skip when zoomed out)
if (!useSimplifiedRendering && (isHovered || isDragging)) {
ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"
ctx.lineWidth = 1
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2 + 1,
screenY - docHeight / 2 + 1,
docWidth - 2,
docHeight - 2,
radius - 1,
)
ctx.stroke()
}
// Highlight ring for search hits
if (isHighlightedDocument) {
ctx.save()
ctx.globalAlpha = 0.9
ctx.strokeStyle = colors.accent.primary
ctx.lineWidth = 3
ctx.setLineDash([6, 4])
const avgDimension = (docWidth + docHeight) / 2
const ringPadding = avgDimension * 0.1
ctx.beginPath()
ctx.roundRect(
screenX - docWidth / 2 - ringPadding,
screenY - docHeight / 2 - ringPadding,
docWidth + ringPadding * 2,
docHeight + ringPadding * 2,
radius + 6,
)
ctx.stroke()
ctx.setLineDash([])
ctx.restore()
}
// Draw document type icon (centered)
if (!useSimplifiedRendering) {
const doc = node.data as ViewportDocument
const iconSize = docHeight * 0.4
drawDocumentIcon(
ctx,
screenX,
screenY,
iconSize,
doc.type || "text",
"rgba(255, 255, 255, 0.8)",
)
}
// Enhanced hover glow effect (skip when zoomed out for performance)
if (!useSimplifiedRendering && (isHovered || isDragging)) {
const glowColor = colors.document.glow
ctx.strokeStyle = glowColor
ctx.lineWidth = 1
ctx.setLineDash([3, 3])
ctx.globalAlpha = 0.6
ctx.beginPath()
const avgDimension = (docWidth + docHeight) / 2
const glowPadding = avgDimension * 0.1
ctx.roundRect(
screenX - docWidth / 2 - glowPadding,
screenY - docHeight / 2 - glowPadding,
docWidth + glowPadding * 2,
docHeight + glowPadding * 2,
15,
)
ctx.stroke()
ctx.setLineDash([])
}
})
ctx.globalAlpha = 1
ctx.restore()
}, [
nodes,
edges,
panX,
panY,
zoom,
width,
height,
highlightDocumentIds,
selectedNodeId,
nodeMap,
])
// Render on changes
const renderKey = useMemo(() => {
const positionHash = nodes.reduce((hash, n) => {
const x = Math.round(n.x * 10)
const y = Math.round(n.y * 10)
const hovered = currentHoveredNode.current === n.id ? 1 : 0
return hash ^ (x + y + hovered)
}, 0)
const highlightHash = (highlightDocumentIds ?? []).reduce((hash, id) => {
return hash ^ id.length
}, 0)
return (
positionHash ^
edges.length ^
Math.round(panX) ^
Math.round(panY) ^
Math.round(zoom * 100) ^
width ^
height ^
highlightHash ^
(selectedNodeId?.length ?? 0)
)
}, [
nodes,
edges.length,
panX,
panY,
zoom,
width,
height,
highlightDocumentIds,
selectedNodeId,
])
const lastRenderKey = useRef<number>(0)
useEffect(() => {
if (renderKey !== lastRenderKey.current) {
lastRenderKey.current = renderKey
render()
}
}, [renderKey, render])
// Add native wheel event listener to prevent browser zoom
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const handleNativeWheel = (e: WheelEvent) => {
e.preventDefault()
e.stopPropagation()
onWheel({
deltaY: e.deltaY,
deltaX: e.deltaX,
clientX: e.clientX,
clientY: e.clientY,
currentTarget: canvas,
nativeEvent: e,
preventDefault: () => {},
stopPropagation: () => {},
} as unknown as React.WheelEvent)
}
canvas.addEventListener("wheel", handleNativeWheel, { passive: false })
const handleGesture = (e: Event) => {
e.preventDefault()
}
canvas.addEventListener("gesturestart", handleGesture, { passive: false })
canvas.addEventListener("gesturechange", handleGesture, {
passive: false,
})
canvas.addEventListener("gestureend", handleGesture, { passive: false })
return () => {
canvas.removeEventListener("wheel", handleNativeWheel)
canvas.removeEventListener("gesturestart", handleGesture)
canvas.removeEventListener("gesturechange", handleGesture)
canvas.removeEventListener("gestureend", handleGesture)
}
}, [onWheel])
// High-DPI handling
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1
useLayoutEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const MAX_CANVAS_SIZE = 16384
const maxDpr =
width > 0 && height > 0
? Math.min(MAX_CANVAS_SIZE / width, MAX_CANVAS_SIZE / height, dpr)
: dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
canvas.width = Math.min(width * maxDpr, MAX_CANVAS_SIZE)
canvas.height = Math.min(height * maxDpr, MAX_CANVAS_SIZE)
const ctx = canvas.getContext("2d")
ctx?.scale(maxDpr, maxDpr)
}, [width, height, dpr])
return (
<canvas
ref={canvasRef}
onClick={handleClick}
onDoubleClick={onDoubleClick}
onMouseDown={handleMouseDown}
onMouseLeave={onPanEnd}
onMouseMove={handleMouseMove}
onMouseUp={onPanEnd}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
style={{
cursor: currentHoveredNode.current ? "pointer" : "move",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
}}
className="absolute inset-0"
/>
)
},
)
ViewportCanvas.displayName = "ViewportCanvas"

View file

@ -0,0 +1,290 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useViewportGraph } from "@/hooks/use-viewport-graph"
import { useViewportInteractions } from "@/hooks/use-viewport-interactions"
import type {
ViewportBounds,
ViewportGraphNode,
} from "@/lib/viewport-graph-types"
import { ViewportCanvas } from "./viewport-canvas"
import { NodePopover } from "./node-popover"
import { NavigationControls } from "./navigation-controls"
import { LoadingIndicator } from "./loading-indicator"
import { GRAPH_SETTINGS } from "./constants"
interface ViewportGraphProps {
containerTags?: string[]
children?: React.ReactNode
}
const INITIAL_VIEWPORT_SIZE = 2000
export function ViewportGraph({ containerTags, children }: ViewportGraphProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
const hasInitialFetchRef = useRef(false)
const hasAutoFittedRef = useRef(false)
const {
documents,
nodes,
edges,
isLoading,
error,
fetchViewport,
totalLoaded,
} = useViewportGraph({
containerTags,
limit: 200,
})
const handleViewportChange = useCallback(
(bounds: ViewportBounds) => {
fetchViewport(bounds)
},
[fetchViewport],
)
const {
panX,
panY,
zoom,
hoveredNode: _hoveredNode,
selectedNode,
handlePanStart,
handlePanMove,
handlePanEnd,
handleWheel,
handleNodeHover,
handleNodeClick,
handleDoubleClick,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
zoomIn,
zoomOut,
autoFitToViewport,
centerViewportOn,
setSelectedNode,
setContainerSize: setInteractionContainerSize,
getWorldBounds,
} = useViewportInteractions({
variant: "consumer",
onViewportChange: handleViewportChange,
})
// Handle container resize
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
const newWidth = containerRef.current.clientWidth
const newHeight = containerRef.current.clientHeight
setContainerSize((prev) => {
if (prev.width !== newWidth || prev.height !== newHeight) {
return { width: newWidth, height: newHeight }
}
return prev
})
setInteractionContainerSize(newWidth, newHeight)
}
}
updateSize()
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(updateSize)
})
if (containerRef.current) {
resizeObserver.observe(containerRef.current)
}
return () => resizeObserver.disconnect()
}, [setInteractionContainerSize])
// Initial fetch - centered on origin with large viewport
useEffect(() => {
if (
!hasInitialFetchRef.current &&
containerSize.width > 0 &&
containerSize.height > 0
) {
hasInitialFetchRef.current = true
const initialBounds: ViewportBounds = {
minX: -INITIAL_VIEWPORT_SIZE / 2,
maxX: INITIAL_VIEWPORT_SIZE / 2,
minY: -INITIAL_VIEWPORT_SIZE / 2,
maxY: INITIAL_VIEWPORT_SIZE / 2,
}
fetchViewport(initialBounds)
}
}, [containerSize.width, containerSize.height, fetchViewport])
// Auto-fit once nodes are loaded
useEffect(() => {
if (
!hasAutoFittedRef.current &&
nodes.length > 0 &&
containerSize.width > 0 &&
containerSize.height > 0
) {
const timer = setTimeout(() => {
autoFitToViewport(nodes, containerSize.width, containerSize.height)
hasAutoFittedRef.current = true
}, 100)
return () => clearTimeout(timer)
}
}, [
nodes.length,
containerSize.width,
containerSize.height,
autoFitToViewport,
])
// Reset auto-fit flag when containerTags change
useEffect(() => {
hasAutoFittedRef.current = false
hasInitialFetchRef.current = false
}, [containerTags?.join(",")])
// Find selected node data
const selectedNodeData = useMemo(() => {
if (!selectedNode) return null
return nodes.find((n) => n.id === selectedNode) ?? null
}, [selectedNode, nodes])
// Calculate popover position
const popoverPosition = useMemo(() => {
if (!selectedNodeData) return null
const screenX = selectedNodeData.x * zoom + panX
const screenY = selectedNodeData.y * zoom + panY
const nodeSize = selectedNodeData.size * zoom
const docWidth = nodeSize * 1.4
// Position popover to the right of the node, or left if near edge
const popoverWidth = 320
const popoverHeight = 400
let x = screenX + docWidth / 2 + 16
let y = screenY - popoverHeight / 2
// Adjust if overflowing right edge
if (x + popoverWidth > containerSize.width) {
x = screenX - docWidth / 2 - popoverWidth - 16
}
// Adjust if overflowing top or bottom
if (y < 16) {
y = 16
} else if (y + popoverHeight > containerSize.height - 16) {
y = containerSize.height - popoverHeight - 16
}
return { x, y }
}, [selectedNodeData, zoom, panX, panY, containerSize])
// Control handlers
const handleCenter = useCallback(() => {
if (nodes.length === 0) return
// Find center of all nodes
let sumX = 0
let sumY = 0
for (const node of nodes) {
sumX += node.x
sumY += node.y
}
const centerX = sumX / nodes.length
const centerY = sumY / nodes.length
centerViewportOn(
centerX,
centerY,
containerSize.width,
containerSize.height,
)
}, [nodes, centerViewportOn, containerSize])
const handleAutoFit = useCallback(() => {
if (nodes.length > 0) {
autoFitToViewport(nodes, containerSize.width, containerSize.height)
}
}, [nodes, autoFitToViewport, containerSize])
if (error) {
return (
<div className="w-full h-full flex items-center justify-center bg-slate-900">
<div className="bg-white/5 backdrop-blur-sm border border-white/20 rounded-xl p-6">
<p className="text-red-400">Error loading graph: {error.message}</p>
</div>
</div>
)
}
return (
<div className="relative w-full h-full bg-slate-900 overflow-hidden">
{/* Loading indicator */}
<LoadingIndicator isLoading={isLoading} totalLoaded={totalLoaded} />
{/* Node popover */}
{selectedNodeData && popoverPosition && (
<NodePopover
node={selectedNodeData}
x={popoverPosition.x}
y={popoverPosition.y}
onClose={() => setSelectedNode(null)}
containerBounds={containerRef.current?.getBoundingClientRect()}
/>
)}
{/* Empty state */}
{!isLoading && nodes.length === 0 && <>{children}</>}
{/* Graph container */}
<div ref={containerRef} className="absolute inset-0">
{containerSize.width > 0 && containerSize.height > 0 && (
<ViewportCanvas
nodes={nodes}
edges={edges}
panX={panX}
panY={panY}
zoom={zoom}
width={containerSize.width}
height={containerSize.height}
onNodeHover={handleNodeHover}
onNodeClick={handleNodeClick}
onPanStart={handlePanStart}
onPanMove={handlePanMove}
onPanEnd={handlePanEnd}
onWheel={handleWheel}
onDoubleClick={handleDoubleClick}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
selectedNodeId={selectedNode}
/>
)}
{/* Navigation controls */}
{containerSize.width > 0 && (
<NavigationControls
onCenter={handleCenter}
onZoomIn={() =>
zoomIn(containerSize.width / 2, containerSize.height / 2)
}
onZoomOut={() =>
zoomOut(containerSize.width / 2, containerSize.height / 2)
}
onAutoFit={handleAutoFit}
nodes={nodes}
/>
)}
</div>
</div>
)
}

View file

@ -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`,

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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;
}
Transforms.setNodes(editor, newProperties);
if (format === "heading" && level && !isActive) {
newProperties.level = level
}
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" />
{/* 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="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 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>
);
)
}

View file

@ -12,7 +12,7 @@ import {
Copy,
RotateCcw,
X,
Square
Square,
} from "lucide-react"
import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"

View file

@ -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>
);
)
}

View file

@ -27,9 +27,7 @@ const steps = [
},
]
export function XBookmarksDetailView({
onBack,
}: XBookmarksDetailViewProps) {
export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) {
const handleInstall = () => {
window.open(
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnangednailhoegogi",

View file

@ -1,41 +1,41 @@
"use client";
"use client"
import { $fetch } from "@lib/api";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useProject } from "@/stores";
import { $fetch } from "@lib/api"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { useProject } from "@/stores"
export function useProjectMutations() {
const queryClient = useQueryClient();
const { selectedProject, setSelectedProject } = useProject();
const queryClient = useQueryClient()
const { selectedProject, setSelectedProject } = useProject()
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: (data) => {
toast.success("Project created successfully!");
queryClient.invalidateQueries({ queryKey: ["projects"] });
toast.success("Project created successfully!")
queryClient.invalidateQueries({ queryKey: ["projects"] })
// Automatically switch to the newly created project
if (data?.containerTag) {
setSelectedProject(data.containerTag);
setSelectedProject(data.containerTag)
}
},
onError: (error) => {
toast.error("Failed to create project", {
description: error instanceof Error ? error.message : "Unknown error",
});
})
},
});
})
const deleteProjectMutation = useMutation({
mutationFn: async ({
@ -43,47 +43,47 @@ export function useProjectMutations() {
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: (_, variables) => {
toast.success("Project deleted successfully");
queryClient.invalidateQueries({ queryKey: ["projects"] });
toast.success("Project deleted successfully")
queryClient.invalidateQueries({ queryKey: ["projects"] })
// If we deleted the selected project, switch to default
const deletedProject = queryClient
.getQueryData<any[]>(["projects"])
?.find((p) => p.id === variables.projectId);
?.find((p) => p.id === variables.projectId)
if (deletedProject?.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",
});
})
},
});
})
const switchProject = (containerTag: string) => {
setSelectedProject(containerTag);
toast.success("Project switched successfully");
};
setSelectedProject(containerTag)
toast.success("Project switched successfully")
}
return {
createProjectMutation,
deleteProjectMutation,
switchProject,
};
}
}

View file

@ -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";
* hasnt 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])
}

View file

@ -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
}

View file

@ -0,0 +1,314 @@
"use client"
import { useCallback, useRef, useState } from "react"
import type {
ViewportDocument,
ViewportEdge,
ViewportGraphNode,
ViewportGraphEdge,
} from "@/lib/viewport-graph-types"
const API_BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
interface TimelineBatch {
type: "batch"
batchIndex: number
documents: ViewportDocument[]
edges: ViewportEdge[]
hasMore: boolean
totalStreamed: number
}
interface TimelineComplete {
type: "complete"
totalDocuments: number
totalEdges: number
}
type TimelineMessage = TimelineBatch | TimelineComplete
interface UseTimelineStreamOptions {
containerTags?: string[]
batchSize?: number
delayBetweenBatches?: number
onBatch?: (nodes: ViewportGraphNode[], edges: ViewportGraphEdge[]) => void
onComplete?: (totalDocuments: number, totalEdges: number) => void
}
interface UseTimelineStreamReturn {
isStreaming: boolean
progress: { streamed: number; total: number | null }
startStream: () => Promise<void>
stopStream: () => void
}
const DOCUMENT_NODE_SIZE = 58
const MEMORY_NODE_SIZE = 40
const NODE_COLOR = "#4f8cff"
const MEMORY_COLOR = "#a78bfa"
const COORDINATE_SCALE = 15
const MEMORY_ORBIT_RADIUS = 80
function normalizeSimilarity(similarity: number): number {
return similarity > 1 ? similarity / 1000 : similarity
}
function getEdgeColor(similarity: number): string {
const normalizedSim = normalizeSimilarity(similarity)
const alpha = 0.2 + normalizedSim * 0.6
return `rgba(100, 149, 237, ${alpha})`
}
function getEdgeThickness(similarity: number): number {
const normalizedSim = normalizeSimilarity(similarity)
return 0.5 + normalizedSim * 2
}
function documentsToNodes(documents: ViewportDocument[]): ViewportGraphNode[] {
const result: ViewportGraphNode[] = []
for (const doc of documents) {
const rawDoc = doc as unknown as Record<string, unknown>
const rawX = Number(rawDoc.spatial_x ?? rawDoc.spatialX ?? rawDoc.x ?? 0)
const rawY = Number(rawDoc.spatial_y ?? rawDoc.spatialY ?? rawDoc.y ?? 0)
const docX = rawX * COORDINATE_SCALE
const docY = rawY * COORDINATE_SCALE
result.push({
id: doc.id,
type: "document" as const,
x: docX,
y: docY,
data: { ...doc, spatialX: docX, spatialY: docY },
size: DOCUMENT_NODE_SIZE,
color: NODE_COLOR,
isHovered: false,
})
const memories = doc.memoryEntries || []
const memoryCount = memories.length
for (let i = 0; i < memoryCount; i++) {
const memory = memories[i]!
const angle = (2 * Math.PI * i) / memoryCount - Math.PI / 2
const memX = docX + Math.cos(angle) * MEMORY_ORBIT_RADIUS
const memY = docY + Math.sin(angle) * MEMORY_ORBIT_RADIUS
result.push({
id: memory.id,
type: "memory" as const,
x: memX,
y: memY,
data: memory,
size: MEMORY_NODE_SIZE,
color: MEMORY_COLOR,
isHovered: false,
parentDocumentId: doc.id,
})
}
}
return result
}
function edgesToGraphEdges(
edges: ViewportEdge[],
documents: ViewportDocument[],
): ViewportGraphEdge[] {
const result: ViewportGraphEdge[] = []
for (const edge of edges) {
const normalizedSim = normalizeSimilarity(edge.similarity)
result.push({
id: `edge-${edge.source}-${edge.target}`,
source: edge.source,
target: edge.target,
similarity: normalizedSim,
color: getEdgeColor(edge.similarity),
opacity: 0.2 + normalizedSim * 0.6,
thickness: getEdgeThickness(edge.similarity),
edgeType: "doc-doc",
})
}
for (const doc of documents) {
const memories = doc.memoryEntries || []
for (const memory of memories) {
result.push({
id: `edge-${doc.id}-${memory.id}`,
source: doc.id,
target: memory.id,
similarity: 1,
color: "rgba(167, 139, 250, 0.4)",
opacity: 0.4,
thickness: 1,
})
}
}
return result
}
export function useTimelineStream({
containerTags,
batchSize = 5,
delayBetweenBatches = 400,
onBatch,
onComplete,
}: UseTimelineStreamOptions = {}): UseTimelineStreamReturn {
const [isStreaming, setIsStreaming] = useState(false)
const [progress, setProgress] = useState<{ streamed: number; total: number | null }>({
streamed: 0,
total: null,
})
const abortControllerRef = useRef<AbortController | null>(null)
const batchQueueRef = useRef<Array<{ nodes: ViewportGraphNode[]; edges: ViewportGraphEdge[]; totalStreamed: number }>>([])
const processingRef = useRef(false)
const processQueue = useCallback(async () => {
if (processingRef.current) return
processingRef.current = true
while (batchQueueRef.current.length > 0) {
const batch = batchQueueRef.current.shift()
if (!batch) break
setProgress({
streamed: batch.totalStreamed,
total: null,
})
onBatch?.(batch.nodes, batch.edges)
if (batchQueueRef.current.length > 0) {
await new Promise((resolve) => setTimeout(resolve, delayBetweenBatches))
}
}
processingRef.current = false
}, [delayBetweenBatches, onBatch])
const stopStream = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
abortControllerRef.current = null
}
batchQueueRef.current = []
processingRef.current = false
setIsStreaming(false)
}, [])
const startStream = useCallback(async () => {
if (isStreaming) return
stopStream()
setIsStreaming(true)
setProgress({ streamed: 0, total: null })
const abortController = new AbortController()
abortControllerRef.current = abortController
try {
const response = await fetch(`${API_BASE_URL}/v3/documents/graph/timeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
containerTags: containerTags?.length ? containerTags : undefined,
batchSize,
}),
signal: abortController.signal,
})
if (!response.ok) {
throw new Error(`Timeline stream failed: ${response.status}`)
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error("No response body")
}
const decoder = new TextDecoder()
let buffer = ""
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() || ""
for (const line of lines) {
if (!line.trim()) continue
try {
const message = JSON.parse(line) as TimelineMessage
if (message.type === "batch") {
const nodes = documentsToNodes(message.documents)
const edges = edgesToGraphEdges(message.edges, message.documents)
batchQueueRef.current.push({
nodes,
edges,
totalStreamed: message.totalStreamed,
})
processQueue()
} else if (message.type === "complete") {
const completeCallback = () => {
setProgress({
streamed: message.totalDocuments,
total: message.totalDocuments,
})
onComplete?.(message.totalDocuments, message.totalEdges)
}
if (batchQueueRef.current.length === 0 && !processingRef.current) {
completeCallback()
} else {
const checkComplete = setInterval(() => {
if (batchQueueRef.current.length === 0 && !processingRef.current) {
clearInterval(checkComplete)
completeCallback()
}
}, 100)
}
}
} catch (parseError) {
console.warn("[timeline-stream] Failed to parse line:", line, parseError)
}
}
}
if (buffer.trim()) {
try {
const message = JSON.parse(buffer) as TimelineMessage
if (message.type === "complete") {
onComplete?.(message.totalDocuments, message.totalEdges)
}
} catch {
// Ignore incomplete final buffer
}
}
} catch (error) {
if ((error as Error).name === "AbortError") {
console.log("[timeline-stream] Stream aborted")
} else {
console.error("[timeline-stream] Error:", error)
}
} finally {
setIsStreaming(false)
abortControllerRef.current = null
}
}, [isStreaming, containerTags, batchSize, onBatch, onComplete, stopStream])
return {
isStreaming,
progress,
startStream,
stopStream,
}
}

View file

@ -0,0 +1,284 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { $fetch } from "@repo/lib/api"
import type {
ViewportDocument,
ViewportEdge,
ViewportBounds,
ViewportGraphNode,
ViewportGraphEdge,
} from "@/lib/viewport-graph-types"
interface UseViewportGraphOptions {
containerTags?: string[]
limit?: number
enabled?: boolean
}
interface UseViewportGraphReturn {
documents: Map<string, ViewportDocument>
nodes: ViewportGraphNode[]
edges: ViewportGraphEdge[]
isLoading: boolean
error: Error | null
fetchViewport: (bounds: ViewportBounds) => Promise<void>
currentViewport: ViewportBounds | null
totalLoaded: number
}
const DOCUMENT_NODE_SIZE = 58
const MEMORY_NODE_SIZE = 40
const NODE_COLOR = "#4f8cff"
const MEMORY_COLOR = "#a78bfa" // Purple for memories
// Backend returns small coordinate values (typically -50 to +50 range)
// Scale up to create visual separation between nodes
const COORDINATE_SCALE = 15
// Distance from parent document for memory nodes
const MEMORY_ORBIT_RADIUS = 80
// Normalize similarity to 0-1 range
// Backend may return similarity as 0-1000 (integer) or 0-1 (float)
function normalizeSimilarity(similarity: number): number {
// If > 1, assume it's in 0-1000 range
return similarity > 1 ? similarity / 1000 : similarity
}
function getEdgeColor(similarity: number): string {
const normalizedSim = normalizeSimilarity(similarity)
const alpha = 0.2 + normalizedSim * 0.6
return `rgba(100, 149, 237, ${alpha})`
}
function getEdgeThickness(similarity: number): number {
const normalizedSim = normalizeSimilarity(similarity)
return 0.5 + normalizedSim * 2
}
export function useViewportGraph({
containerTags,
limit = 200,
enabled = true,
}: UseViewportGraphOptions = {}): UseViewportGraphReturn {
const [documents, setDocuments] = useState<Map<string, ViewportDocument>>(
new Map(),
)
const [rawEdges, setRawEdges] = useState<ViewportEdge[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [currentViewport, setCurrentViewport] = useState<ViewportBounds | null>(
null,
)
const fetchInProgressRef = useRef(false)
const lastFetchParamsRef = useRef<string>("")
const fetchViewport = useCallback(
async (bounds: ViewportBounds) => {
if (!enabled) return
if (fetchInProgressRef.current) return
// Guard against invalid bounds (NaN or Infinity)
if (
!Number.isFinite(bounds.minX) ||
!Number.isFinite(bounds.maxX) ||
!Number.isFinite(bounds.minY) ||
!Number.isFinite(bounds.maxY)
) {
console.warn("[viewport-graph] Skipping fetch with invalid bounds:", bounds)
return
}
const fetchParams = JSON.stringify({
bounds,
containerTags,
limit,
})
if (fetchParams === lastFetchParamsRef.current) return
fetchInProgressRef.current = true
lastFetchParamsRef.current = fetchParams
setIsLoading(true)
setError(null)
try {
const requestBody = {
viewport: {
minX: bounds.minX,
maxX: bounds.maxX,
minY: bounds.minY,
maxY: bounds.maxY,
},
containerTags: containerTags?.length ? containerTags : undefined,
limit,
}
console.log("[viewport-graph] Fetching with body:", requestBody)
const response = await $fetch("@post/documents/graph/viewport", {
body: requestBody,
disableValidation: true,
})
console.log("[viewport-graph] Response:", response)
if (response.error) {
console.error("[viewport-graph] Error:", response.error)
throw new Error(response.error?.message || "Failed to fetch viewport")
}
const data = response.data as {
documents: ViewportDocument[]
edges: ViewportEdge[]
viewport: ViewportBounds
}
console.log("[viewport-graph] Documents received:", data.documents?.length)
console.log("[viewport-graph] Edges received:", data.edges?.length)
if (data.documents?.length > 0) {
console.log("[viewport-graph] Sample document:", data.documents[0])
}
setDocuments((prev) => {
const next = new Map(prev)
for (const doc of data.documents) {
next.set(doc.id, doc)
}
return next
})
setRawEdges(data.edges)
setCurrentViewport(data.viewport)
} catch (err) {
setError(err instanceof Error ? err : new Error("Unknown error"))
} finally {
setIsLoading(false)
fetchInProgressRef.current = false
}
},
[containerTags, limit, enabled],
)
// Reset when containerTags change
useEffect(() => {
setDocuments(new Map())
setRawEdges([])
setCurrentViewport(null)
lastFetchParamsRef.current = ""
}, [containerTags?.join(",")])
// Convert documents and memories to nodes
const nodes = useMemo((): ViewportGraphNode[] => {
const result: ViewportGraphNode[] = []
for (const doc of documents.values()) {
// Handle different possible field names from backend (snake_case from API)
const rawDoc = doc as unknown as Record<string, unknown>
const rawX = Number(rawDoc.spatial_x ?? rawDoc.spatialX ?? rawDoc.x ?? 0)
const rawY = Number(rawDoc.spatial_y ?? rawDoc.spatialY ?? rawDoc.y ?? 0)
// Scale up coordinates to create visual separation
const docX = rawX * COORDINATE_SCALE
const docY = rawY * COORDINATE_SCALE
// Add document node
result.push({
id: doc.id,
type: "document" as const,
x: docX,
y: docY,
data: { ...doc, spatialX: docX, spatialY: docY },
size: DOCUMENT_NODE_SIZE,
color: NODE_COLOR,
isHovered: false,
})
// Add memory nodes orbiting around the document
const memories = doc.memoryEntries || []
const memoryCount = memories.length
for (let i = 0; i < memoryCount; i++) {
const memory = memories[i]!
// Position memories in a circle around the parent document
const angle = (2 * Math.PI * i) / memoryCount - Math.PI / 2 // Start from top
const memX = docX + Math.cos(angle) * MEMORY_ORBIT_RADIUS
const memY = docY + Math.sin(angle) * MEMORY_ORBIT_RADIUS
result.push({
id: memory.id,
type: "memory" as const,
x: memX,
y: memY,
data: memory,
size: MEMORY_NODE_SIZE,
color: MEMORY_COLOR,
isHovered: false,
parentDocumentId: doc.id,
})
}
}
// Debug: log coordinate range
if (result.length > 0) {
const docNodes = result.filter(n => n.type === "document")
const memNodes = result.filter(n => n.type === "memory")
console.log("[viewport-graph] Nodes:", {
documents: docNodes.length,
memories: memNodes.length,
total: result.length,
})
}
return result
}, [documents])
// Convert raw edges to graph edges, including doc-doc and doc-memory connections
const edges = useMemo((): ViewportGraphEdge[] => {
const docIds = new Set(documents.keys())
const result: ViewportGraphEdge[] = []
// Add doc-doc edges from API
for (const edge of rawEdges) {
if (docIds.has(edge.source) && docIds.has(edge.target)) {
const normalizedSim = normalizeSimilarity(edge.similarity)
result.push({
id: `edge-${edge.source}-${edge.target}`,
source: edge.source,
target: edge.target,
similarity: normalizedSim,
color: getEdgeColor(edge.similarity),
opacity: 0.2 + normalizedSim * 0.6,
thickness: getEdgeThickness(edge.similarity),
edgeType: "doc-doc",
})
}
}
// Add doc-memory edges (connecting documents to their memories)
for (const doc of documents.values()) {
const memories = doc.memoryEntries || []
for (const memory of memories) {
result.push({
id: `edge-${doc.id}-${memory.id}`,
source: doc.id,
target: memory.id,
similarity: 1, // Strong connection
color: "rgba(167, 139, 250, 0.4)",
opacity: 0.4,
thickness: 1,
})
}
}
return result
}, [rawEdges, documents])
return {
documents,
nodes,
edges,
isLoading,
error,
fetchViewport,
currentViewport,
totalLoaded: documents.size,
}
}

View file

@ -0,0 +1,534 @@
"use client"
import { useCallback, useRef, useState } from "react"
import type {
ViewportBounds,
ViewportGraphNode,
} from "@/lib/viewport-graph-types"
import { GRAPH_SETTINGS } from "@/components/viewport-graph/constants"
type Variant = "console" | "consumer"
interface ViewportInteractionsOptions {
variant?: Variant
initialZoom?: number
initialPanX?: number
initialPanY?: number
onViewportChange?: (bounds: ViewportBounds) => void
}
export function useViewportInteractions({
variant = "consumer",
initialZoom,
initialPanX,
initialPanY,
onViewportChange,
}: ViewportInteractionsOptions = {}) {
const settings = GRAPH_SETTINGS[variant]
const [panX, setPanX] = useState(initialPanX ?? settings.initialPanX)
const [panY, setPanY] = useState(initialPanY ?? settings.initialPanY)
const [zoom, setZoom] = useState(initialZoom ?? settings.initialZoom)
const [isPanning, setIsPanning] = useState(false)
const [panStart, setPanStart] = useState({ x: 0, y: 0 })
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
const [selectedNode, setSelectedNode] = useState<string | null>(null)
// Animation state for smooth transitions
const animationRef = useRef<number | null>(null)
const [isAnimating, setIsAnimating] = useState(false)
// Smooth animation helper
const animateToViewState = useCallback(
(
targetPanX: number,
targetPanY: number,
targetZoom: number,
duration = 300,
) => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
const startPanX = panX
const startPanY = panY
const startZoom = zoom
const startTime = Date.now()
setIsAnimating(true)
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
const easeOut = 1 - (1 - progress) ** 3
const currentPanX = startPanX + (targetPanX - startPanX) * easeOut
const currentPanY = startPanY + (targetPanY - startPanY) * easeOut
const currentZoom = startZoom + (targetZoom - startZoom) * easeOut
setPanX(currentPanX)
setPanY(currentPanY)
setZoom(currentZoom)
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate)
} else {
setIsAnimating(false)
animationRef.current = null
}
}
animate()
},
[panX, panY, zoom],
)
// Touch gesture state
const [touchState, setTouchState] = useState<{
touches: { id: number; x: number; y: number }[]
lastDistance: number
lastCenter: { x: number; y: number }
isGesturing: boolean
}>({
touches: [],
lastDistance: 0,
lastCenter: { x: 0, y: 0 },
isGesturing: false,
})
// Debounce viewport change callbacks
const viewportChangeTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const containerSizeRef = useRef({ width: 0, height: 0 })
// Calculate world bounds from screen viewport
const getWorldBounds = useCallback(
(width: number, height: number): ViewportBounds => {
const minX = -panX / zoom
const minY = -panY / zoom
const maxX = (width - panX) / zoom
const maxY = (height - panY) / zoom
return { minX, maxX, minY, maxY }
},
[panX, panY, zoom],
)
// Trigger viewport change with debounce
const triggerViewportChange = useCallback(() => {
if (!onViewportChange) return
if (viewportChangeTimeoutRef.current) {
clearTimeout(viewportChangeTimeoutRef.current)
}
viewportChangeTimeoutRef.current = setTimeout(() => {
const { width, height } = containerSizeRef.current
if (width > 0 && height > 0) {
const bounds = getWorldBounds(width, height)
// Validate bounds before calling callback
if (
Number.isFinite(bounds.minX) &&
Number.isFinite(bounds.maxX) &&
Number.isFinite(bounds.minY) &&
Number.isFinite(bounds.maxY)
) {
onViewportChange(bounds)
}
}
}, 150)
}, [onViewportChange, getWorldBounds])
// Update container size ref
const setContainerSize = useCallback((width: number, height: number) => {
containerSizeRef.current = { width, height }
}, [])
// Pan handlers
const handlePanStart = useCallback(
(e: React.MouseEvent) => {
setIsPanning(true)
setPanStart({ x: e.clientX - panX, y: e.clientY - panY })
},
[panX, panY],
)
const handlePanMove = useCallback(
(e: React.MouseEvent) => {
if (!isPanning) return
const newPanX = e.clientX - panStart.x
const newPanY = e.clientY - panStart.y
setPanX(newPanX)
setPanY(newPanY)
},
[isPanning, panStart],
)
const handlePanEnd = useCallback(() => {
if (isPanning) {
setIsPanning(false)
triggerViewportChange()
}
}, [isPanning, triggerViewportChange])
// Zoom handlers
const handleWheel = useCallback(
(e: React.WheelEvent) => {
e.preventDefault()
e.stopPropagation()
// Handle horizontal scrolling (trackpad swipe) by converting to pan
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
const panDelta = e.deltaX * 0.5
setPanX((prev) => prev - panDelta)
triggerViewportChange()
return
}
// Vertical scroll - zoom behavior
const delta = e.deltaY > 0 ? 0.97 : 1.03
const newZoom = Math.max(0.05, Math.min(3, zoom * delta))
// Get mouse position relative to the viewport
let mouseX = e.clientX
let mouseY = e.clientY
const target = e.currentTarget
if (target && "getBoundingClientRect" in target) {
const rect = target.getBoundingClientRect()
mouseX = e.clientX - rect.left
mouseY = e.clientY - rect.top
}
// Calculate the world position of the mouse cursor
const worldX = (mouseX - panX) / zoom
const worldY = (mouseY - panY) / zoom
// Calculate new pan to keep the mouse position stationary
const newPanX = mouseX - worldX * newZoom
const newPanY = mouseY - worldY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
triggerViewportChange()
},
[zoom, panX, panY, triggerViewportChange],
)
const zoomIn = useCallback(
(centerX?: number, centerY?: number) => {
const zoomFactor = 1.2
const newZoom = Math.min(3, zoom * zoomFactor)
if (centerX !== undefined && centerY !== undefined) {
const worldX = (centerX - panX) / zoom
const worldY = (centerY - panY) / zoom
const newPanX = centerX - worldX * newZoom
const newPanY = centerY - worldY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
} else {
setZoom(newZoom)
}
triggerViewportChange()
},
[zoom, panX, panY, triggerViewportChange],
)
const zoomOut = useCallback(
(centerX?: number, centerY?: number) => {
const zoomFactor = 0.8
const newZoom = Math.max(0.05, zoom * zoomFactor)
if (centerX !== undefined && centerY !== undefined) {
const worldX = (centerX - panX) / zoom
const worldY = (centerY - panY) / zoom
const newPanX = centerX - worldX * newZoom
const newPanY = centerY - worldY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
} else {
setZoom(newZoom)
}
triggerViewportChange()
},
[zoom, panX, panY, triggerViewportChange],
)
// Auto-fit to show all nodes
const autoFitToViewport = useCallback(
(
nodes: ViewportGraphNode[],
viewportWidth: number,
viewportHeight: number,
options?: { padding?: number },
) => {
if (nodes.length === 0) return
const padding = options?.padding ?? 100
// Calculate bounding box of all nodes
let minX = Number.POSITIVE_INFINITY
let maxX = Number.NEGATIVE_INFINITY
let minY = Number.POSITIVE_INFINITY
let maxY = Number.NEGATIVE_INFINITY
for (const node of nodes) {
minX = Math.min(minX, node.x - node.size)
maxX = Math.max(maxX, node.x + node.size)
minY = Math.min(minY, node.y - node.size)
maxY = Math.max(maxY, node.y + node.size)
}
const contentWidth = maxX - minX
const contentHeight = maxY - minY
const contentCenterX = (minX + maxX) / 2
const contentCenterY = (minY + maxY) / 2
// Calculate zoom to fit content with padding
const scaleX = (viewportWidth - padding * 2) / contentWidth
const scaleY = (viewportHeight - padding * 2) / contentHeight
const newZoom = Math.min(Math.max(0.05, Math.min(scaleX, scaleY)), 2)
// Center content
const newPanX = viewportWidth / 2 - contentCenterX * newZoom
const newPanY = viewportHeight / 2 - contentCenterY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
triggerViewportChange()
},
[triggerViewportChange],
)
// Center viewport on specific world position
const centerViewportOn = useCallback(
(
worldX: number,
worldY: number,
viewportWidth: number,
viewportHeight: number,
) => {
const newPanX = viewportWidth / 2 - worldX * zoom
const newPanY = viewportHeight / 2 - worldY * zoom
setPanX(newPanX)
setPanY(newPanY)
triggerViewportChange()
},
[zoom, triggerViewportChange],
)
// Touch gesture handlers
const handleTouchStart = useCallback((e: React.TouchEvent) => {
const touches = Array.from(e.touches).map((touch) => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY,
}))
if (touches.length >= 2) {
const touch1 = touches[0]!
const touch2 = touches[1]!
const distance = Math.sqrt(
(touch2.x - touch1.x) ** 2 + (touch2.y - touch1.y) ** 2,
)
const center = {
x: (touch1.x + touch2.x) / 2,
y: (touch1.y + touch2.y) / 2,
}
setTouchState({
touches,
lastDistance: distance,
lastCenter: center,
isGesturing: true,
})
} else {
setTouchState((prev) => ({ ...prev, touches, isGesturing: false }))
}
}, [])
const handleTouchMove = useCallback(
(e: React.TouchEvent) => {
e.preventDefault()
const touches = Array.from(e.touches).map((touch) => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY,
}))
if (touches.length >= 2 && touchState.isGesturing) {
const touch1 = touches[0]!
const touch2 = touches[1]!
const distance = Math.sqrt(
(touch2.x - touch1.x) ** 2 + (touch2.y - touch1.y) ** 2,
)
const center = {
x: (touch1.x + touch2.x) / 2,
y: (touch1.y + touch2.y) / 2,
}
const distanceChange = distance / touchState.lastDistance
const newZoom = Math.max(0.05, Math.min(3, zoom * distanceChange))
const canvas = e.currentTarget as HTMLElement
const rect = canvas.getBoundingClientRect()
const centerX = center.x - rect.left
const centerY = center.y - rect.top
const worldX = (centerX - panX) / zoom
const worldY = (centerY - panY) / zoom
const newPanX = centerX - worldX * newZoom
const newPanY = centerY - worldY * newZoom
const centerDx = center.x - touchState.lastCenter.x
const centerDy = center.y - touchState.lastCenter.y
setZoom(newZoom)
setPanX(newPanX + centerDx)
setPanY(newPanY + centerDy)
setTouchState({
touches,
lastDistance: distance,
lastCenter: center,
isGesturing: true,
})
} else if (touches.length === 1 && !touchState.isGesturing && isPanning) {
const touch = touches[0]!
const newPanX = touch.x - panStart.x
const newPanY = touch.y - panStart.y
setPanX(newPanX)
setPanY(newPanY)
}
},
[touchState, zoom, panX, panY, isPanning, panStart],
)
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
const touches = Array.from(e.touches).map((touch) => ({
id: touch.identifier,
x: touch.clientX,
y: touch.clientY,
}))
if (touches.length < 2) {
setTouchState((prev) => ({ ...prev, touches, isGesturing: false }))
} else {
setTouchState((prev) => ({ ...prev, touches }))
}
if (touches.length === 0) {
setIsPanning(false)
triggerViewportChange()
}
},
[triggerViewportChange],
)
// Double-click to zoom in
const handleDoubleClick = useCallback(
(e: React.MouseEvent) => {
const zoomFactor = 1.5
const newZoom = Math.min(3, zoom * zoomFactor)
let mouseX = e.clientX
let mouseY = e.clientY
const target = e.currentTarget
if (target && "getBoundingClientRect" in target) {
const rect = target.getBoundingClientRect()
mouseX = e.clientX - rect.left
mouseY = e.clientY - rect.top
}
const worldX = (mouseX - panX) / zoom
const worldY = (mouseY - panY) / zoom
const newPanX = mouseX - worldX * newZoom
const newPanY = mouseY - worldY * newZoom
setZoom(newZoom)
setPanX(newPanX)
setPanY(newPanY)
triggerViewportChange()
},
[zoom, panX, panY, triggerViewportChange],
)
// Reset view to initial settings
const resetView = useCallback(
(animate = true) => {
if (animate && !isAnimating) {
animateToViewState(
settings.initialPanX,
settings.initialPanY,
settings.initialZoom,
300,
)
} else {
setPanX(settings.initialPanX)
setPanY(settings.initialPanY)
setZoom(settings.initialZoom)
}
triggerViewportChange()
},
[settings, isAnimating, animateToViewState, triggerViewportChange],
)
// Node interaction handlers
const handleNodeHover = useCallback((nodeId: string | null) => {
setHoveredNode(nodeId)
}, [])
const handleNodeClick = useCallback(
(nodeId: string) => {
setSelectedNode(selectedNode === nodeId ? null : nodeId)
},
[selectedNode],
)
return {
// State
panX,
panY,
zoom,
hoveredNode,
selectedNode,
isPanning,
isAnimating,
// Handlers
handlePanStart,
handlePanMove,
handlePanEnd,
handleWheel,
handleNodeHover,
handleNodeClick,
handleDoubleClick,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
// Controls
zoomIn,
zoomOut,
resetView,
autoFitToViewport,
centerViewportOn,
setSelectedNode,
setContainerSize,
getWorldBounds,
}
}

View file

@ -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

View file

@ -2,9 +2,9 @@ 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)
}
if (posthog.__loaded) {
posthog.capture(eventName, properties)
}
}
export const analytics = {

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,108 @@
// Types for viewport-based graph API
export interface ViewportDocument {
id: string
customId?: string | null
title?: string | null
summary?: string | null
url?: string | null
source?: string | null
type?: string | null
status: string
metadata?: Record<string, string | number | boolean> | null
createdAt: string | Date
updatedAt: string | Date
containerTags?: string[] | null
// Spatial position from PostGIS
spatialX: number
spatialY: number
// Memory entries for this document
memoryEntries: ViewportMemoryEntry[]
}
export interface ViewportMemoryEntry {
id: string
customId?: string | null
documentId: string
content: string | null
summary?: string | null
title?: string | null
url?: string | null
type?: string | null
createdAt: string | Date
updatedAt: string | Date
spaceContainerTag?: string | null
spaceId?: string | null
}
export interface ViewportEdge {
source: string
target: string
similarity: number
}
export interface ViewportBounds {
minX: number
maxX: number
minY: number
maxY: number
}
export interface ViewportResponse {
documents: ViewportDocument[]
edges: ViewportEdge[]
viewport: ViewportBounds
timestamp: string
}
export interface ViewportGraphNode {
id: string
type: "document" | "memory"
x: number
y: number
data: ViewportDocument | ViewportMemoryEntry
size: number
color: string
isHovered: boolean
isDragging?: boolean
parentDocumentId?: string // For memory nodes, references parent document
}
export interface ViewportGraphEdge {
id: string
source: string | ViewportGraphNode
target: string | ViewportGraphNode
similarity: number
color: string
opacity: number
thickness: number
edgeType?: "doc-doc"
visualProps?: {
opacity: number
thickness: number
glow: number
pulseDuration: number
}
}
export interface ViewportCanvasProps {
nodes: ViewportGraphNode[]
edges: ViewportGraphEdge[]
panX: number
panY: number
zoom: number
width: number
height: number
onNodeHover: (nodeId: string | null) => void
onNodeClick: (nodeId: string) => void
onPanStart: (e: React.MouseEvent) => void
onPanMove: (e: React.MouseEvent) => void
onPanEnd: () => void
onWheel: (e: React.WheelEvent) => void
onDoubleClick: (e: React.MouseEvent) => void
onTouchStart?: (e: React.TouchEvent) => void
onTouchMove?: (e: React.TouchEvent) => void
onTouchEnd?: (e: React.TouchEvent) => void
highlightDocumentIds?: string[]
selectedNodeId?: string | null
}

View file

@ -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)

View file

@ -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,
});
})

View file

@ -1,5 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
}
export default config;
export default config

View file

@ -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

View file

@ -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 }
}

View file

@ -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)
},
}

View file

@ -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"]
}

View file

@ -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"
}
]
}

406
bun.lock
View file

@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "supermemory",
@ -118,21 +119,6 @@
"typescript": "^5",
},
},
"apps/raycast-extension": {
"name": "supermemory",
"dependencies": {
"@raycast/api": "^1.103.3",
"@raycast/utils": "^2.2.1",
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
"@types/node": "22.13.10",
"@types/react": "19.0.10",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2",
},
},
"apps/web": {
"name": "@repo/web",
"version": "0.1.0",
@ -751,24 +737,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@essentials/memoize-one": ["@essentials/memoize-one@1.1.0", "", {}, "sha512-HMkuIkKNe0EWSUpZhlaq9+5Yp47YhrMhxLMnXTRnEyE5N4xKLspAvMGjUFdi794VnEF1EcOZFS8rdROeujrgag=="],
"@essentials/one-key-map": ["@essentials/one-key-map@1.2.0", "", {}, "sha512-C2H7zHVcsoipDv4VKY5uUcv5ilsK+uEgEj+WeOdN5oz/Qj1/OZIzCdle90gDzj0xnGQrmZ9qDujwD7AkBb5k9A=="],
@ -793,14 +761,6 @@
"@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
@ -1013,14 +973,6 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@oclif/core": ["@oclif/core@4.8.0", "", { "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-jteNUQKgJHLHFbbz806aGZqf+RJJ7t4gwF4MYa8fCwCxQ8/klJNWc0MvaJiBebk7Mc+J39mdlsB4XraaCKznFw=="],
"@oclif/plugin-autocomplete": ["@oclif/plugin-autocomplete@3.2.39", "", { "dependencies": { "@oclif/core": "^4", "ansis": "^3.16.0", "debug": "^4.4.1", "ejs": "^3.1.10" } }, "sha512-OwAZNnSpuDjKyhAwoOJkFWxGswPFKBB4hpNIMsj6PUtbKwGBPmD+2wGGPgTsDioVwLmUELSb2bZ+1dxHfvXmvg=="],
"@oclif/plugin-help": ["@oclif/plugin-help@6.2.36", "", { "dependencies": { "@oclif/core": "^4" } }, "sha512-NBQIg5hEMhvdbi4mSrdqRGl5XJ0bqTAHq6vDCCCDXUcfVtdk3ZJbSxtRVWyVvo9E28vwqu6MZyHOJylevqcHbA=="],
"@oclif/plugin-not-found": ["@oclif/plugin-not-found@3.2.73", "", { "dependencies": { "@inquirer/prompts": "^7.10.1", "@oclif/core": "^4.8.0", "ansis": "^3.17.0", "fast-levenshtein": "^3.0.0" } }, "sha512-2bQieTGI9XNFe9hKmXQjJmHV5rZw+yn7Rud1+C5uLEo8GaT89KZbiLTJgL35tGILahy/cB6+WAs812wjw7TK6w=="],
"@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
"@octokit/core": ["@octokit/core@6.1.6", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA=="],
@ -1241,14 +1193,6 @@
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@raycast/api": ["@raycast/api@1.104.1", "", { "dependencies": { "@oclif/core": "^4.5.4", "@oclif/plugin-autocomplete": "^3.2.35", "@oclif/plugin-help": "^6.2.33", "@oclif/plugin-not-found": "^3.2.68", "@types/node": "22.13.10", "@types/react": "19.0.10", "esbuild": "^0.25.10", "react": "19.0.0" }, "peerDependencies": { "react-devtools": "6.1.1" }, "optionalPeers": ["react-devtools"], "bin": { "ray": "bin/run.js" } }, "sha512-5v52JDzAAoCA6/JOoBfsgCXK4fzIY02lvMH0IA3ghuxZuk8i2ID2rtPkTuIAbebpkILADB7gP4yaBphkMLiCJA=="],
"@raycast/eslint-config": ["@raycast/eslint-config@2.1.1", "", { "dependencies": { "@eslint/js": "^9.36.0", "@raycast/eslint-plugin": "^2.1.1", "eslint-config-prettier": "^10.1.8", "globals": "^16.4.0", "typescript-eslint": "^8.45.0" }, "peerDependencies": { "eslint": ">=8.23.0", "prettier": ">=2", "typescript": ">=4" } }, "sha512-W0kxF+FJ+BYQn0EKIV739j2ZrHEtjo/LclsoZgUWg3t364Dq75XKcjqYFYx+59/DBaamY0amdajlfuDAf6veAg=="],
"@raycast/eslint-plugin": ["@raycast/eslint-plugin@2.1.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.26.1" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-r2gs8uIlNp6I2mLOyN/kReGlvigzEeuyQPl4yw7nwLy8Zxjfjhg8txMViaBux8juBWBxbSWq/IfW6ZA50oeOHQ=="],
"@raycast/utils": ["@raycast/utils@2.2.2", "", { "dependencies": { "dequal": "^2.0.3" }, "peerDependencies": { "@raycast/api": ">=1.99.4", "react": ">=19.0.0" }, "optionalPeers": ["react"] }, "sha512-tZcyWCHZvz4L/i1CGEnSZkBoK6wwX1pzlTKjcWWugbrQyG0QCMOxjKJfRC/iNkD+hHaqhMWUj4Y0LNo/NknvFw=="],
"@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
"@react-hook/debounce": ["@react-hook/debounce@3.0.0", "", { "dependencies": { "@react-hook/latest": "^1.0.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-ir/kPrSfAzY12Gre0sOHkZ2rkEmM4fS5M5zFxCi4BnCeXh2nvx9Ujd+U4IGpKCuPA+EQD0pg1eK2NGLvfWejag=="],
@ -1555,7 +1499,7 @@
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
"@smithy/util-waiter": ["@smithy/util-waiter@2.2.0", "", { "dependencies": { "@smithy/abort-controller": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-IHk53BVw6MPMi2Gsn+hCng8rFA3ZmR3Rk7GllxDUW9qFJl/hiSvskn7XldkECapQVkIg/1dHpMAxI9xSTaLLSA=="],
"@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="],
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
@ -1843,26 +1787,6 @@
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@ -1981,7 +1905,7 @@
"ai-gateway-provider": ["ai-gateway-provider@0.0.11", "", { "dependencies": { "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "ai": "^4.3.16" } }, "sha512-OrovxjYP+yowh4/OEsd/cYkvNx7s0mhBRXBO9RLyN8i803jlxyLlWZ1OAbiGL5FET3cnI8pYb8TrFMmZQT9yZQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
@ -1997,7 +1921,7 @@
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
@ -2207,7 +2131,7 @@
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clean-stack": ["clean-stack@3.0.1", "", { "dependencies": { "escape-string-regexp": "4.0.0" } }, "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg=="],
"clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
@ -2417,8 +2341,6 @@
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deep-object-diff": ["deep-object-diff@1.1.9", "", {}, "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
@ -2519,8 +2441,6 @@
"efrt": ["efrt@2.7.0", "", {}, "sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
@ -2591,27 +2511,17 @@
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
@ -2675,10 +2585,6 @@
"fast-json-patch": ["fast-json-patch@3.1.1", "", {}, "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-memoize": ["fast-memoize@2.5.2", "", {}, "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw=="],
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
@ -2687,8 +2593,6 @@
"fast-xml-parser": ["fast-xml-parser@5.3.3", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA=="],
"fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="],
@ -2705,14 +2609,10 @@
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
"filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="],
"filesize": ["filesize@11.0.13", "", {}, "sha512-mYJ/qXKvREuO0uH8LTQJ6v7GsUvVOguqxg2VTwQUkyTPXXRRWPdjuUPVqdBrJQhvci48OHlNGRnux+Slr2Rnvw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@ -2725,10 +2625,6 @@
"firefox-profile": ["firefox-profile@4.7.0", "", { "dependencies": { "adm-zip": "~0.5.x", "fs-extra": "^11.2.0", "ini": "^4.1.3", "minimist": "^1.2.8", "xml2js": "^0.6.2" }, "bin": { "firefox-profile": "lib/cli.js" } }, "sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"flubber": ["flubber@0.4.2", "", { "dependencies": { "d3-array": "^1.2.0", "d3-polygon": "^1.0.3", "earcut": "^2.1.1", "svg-path-properties": "^0.2.1", "svgpath": "^2.2.1", "topojson-client": "^3.0.0" } }, "sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
@ -2791,8 +2687,6 @@
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="],
"get-port": ["get-port@5.1.1", "", {}, "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="],
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
@ -2813,14 +2707,12 @@
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="],
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
@ -2959,9 +2851,7 @@
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@ -3105,8 +2995,6 @@
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
"javascript-stringify": ["javascript-stringify@2.1.0", "", {}, "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
@ -3139,14 +3027,12 @@
"json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"json-schema-walker": ["json-schema-walker@2.0.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.1.0", "clone": "^2.1.2" } }, "sha512-nXN2cMky0Iw7Af28w061hmxaPDaML5/bQD9nwm1lOoIKEGjHcRGxqWe4MfrkYThYAPjSUhmsp4bJNoLAyVn9Xw=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonc-parser": ["jsonc-parser@2.2.1", "", {}, "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w=="],
@ -3189,8 +3075,6 @@
"leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"libsodium": ["libsodium@0.7.16", "", {}, "sha512-3HrzSPuzm6Yt9aTYCDxYEG8x8/6C0+ag655Y7rhhWZM9PT4NpdnbqlzXhGZlDnkgR6MeSTnOt/VIyHLs9aSf+Q=="],
"libsodium-wrappers": ["libsodium-wrappers@0.7.16", "", { "dependencies": { "libsodium": "^0.7.16" } }, "sha512-Gtr/WBx4dKjvRL1pvfwZqu7gO6AfrQ0u9vFL+kXihtHf6NfkROR8pjYWn98MFDI3jN19Ii1ZUfPR9afGiPyfHg=="],
@ -3223,7 +3107,7 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@2.0.4", "", {}, "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A=="],
@ -3483,8 +3367,6 @@
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
@ -3567,8 +3449,6 @@
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
"os-shim": ["os-shim@0.1.3", "", {}, "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A=="],
@ -3723,8 +3603,6 @@
"preact": ["preact@10.28.2", "", {}, "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
@ -3759,8 +3637,6 @@
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="],
"puppeteer": ["puppeteer@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.14.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw=="],
@ -4127,7 +4003,7 @@
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="],
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
@ -4151,7 +4027,7 @@
"suffix-thumb": ["suffix-thumb@5.0.2", "", {}, "sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA=="],
"supermemory": ["supermemory@workspace:apps/raycast-extension"],
"supermemory": ["supermemory@3.10.0", "", {}, "sha512-xUTn6ElIIXwizj80ELDFgXjAcBpV9LtNz7kWl+PVQfVzFHM2lOguFHaDx6GoWuWW2GpnE3ikkKPCsKpvHFgqgg=="],
"supermemory-browser-extension": ["supermemory-browser-extension@workspace:apps/browser-extension"],
@ -4241,8 +4117,6 @@
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
@ -4275,8 +4149,6 @@
"twoslash-protocol": ["twoslash-protocol@0.3.6", "", {}, "sha512-FHGsJ9Q+EsNr5bEbgG3hnbkvEBdW5STgPU824AHUjB4kw0Dn4p8tABT7Ncg1Ie6V0+mDg3Qpy41VafZXcQhWMA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
@ -4293,8 +4165,6 @@
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="],
"ufo": ["ufo@1.6.2", "", {}, "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q=="],
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
@ -4361,8 +4231,6 @@
"update-notifier": ["update-notifier@7.3.1", "", { "dependencies": { "boxen": "^8.0.1", "chalk": "^5.3.0", "configstore": "^7.0.0", "is-in-ci": "^1.0.0", "is-installed-globally": "^1.0.0", "is-npm": "^6.0.0", "latest-version": "^9.0.0", "pupa": "^3.1.0", "semver": "^7.6.3", "xdg-basedir": "^5.1.0" } }, "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="],
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
@ -4457,10 +4325,6 @@
"winreg": ["winreg@0.0.12", "", {}, "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
"workerd": ["workerd@1.20260107.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260107.1", "@cloudflare/workerd-darwin-arm64": "1.20260107.1", "@cloudflare/workerd-linux-64": "1.20260107.1", "@cloudflare/workerd-linux-arm64": "1.20260107.1", "@cloudflare/workerd-windows-64": "1.20260107.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-4ylAQJDdJZdMAUl2SbJgTa77YHpa88l6qmhiuCLNactP933+rifs7I0w1DslhUIFgydArUX5dNLAZnZhT7Bh7g=="],
"wrangler": ["wrangler@4.58.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.8.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260107.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260107.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260107.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-Jm6EYtlt8iUcznOCPSMYC54DYkwrMNESzbH0Vh3GFHv/7XVw5gBC13YJAB+nWMRGJ+6B2dMzy/NVQS4ONL51Pw=="],
@ -4561,8 +4425,6 @@
"@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"@asyncapi/parser/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@asyncapi/parser/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@asyncapi/parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@ -4647,14 +4509,10 @@
"@aws-sdk/client-cloudfront/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-cloudfront/@smithy/util-waiter": ["@smithy/util-waiter@2.2.0", "", { "dependencies": { "@smithy/abort-controller": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-IHk53BVw6MPMi2Gsn+hCng8rFA3ZmR3Rk7GllxDUW9qFJl/hiSvskn7XldkECapQVkIg/1dHpMAxI9xSTaLLSA=="],
"@aws-sdk/client-cloudfront/fast-xml-parser": ["fast-xml-parser@4.2.5", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g=="],
"@aws-sdk/client-dynamodb/@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="],
"@aws-sdk/client-lambda/@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="],
"@aws-sdk/client-s3/@smithy/util-waiter": ["@smithy/util-waiter@4.2.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw=="],
"@aws-sdk/client-sts/@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@3.0.0", "", { "dependencies": { "@aws-crypto/ie11-detection": "^3.0.0", "@aws-crypto/sha256-js": "^3.0.0", "@aws-crypto/supports-web-crypto": "^3.0.0", "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ=="],
"@aws-sdk/client-sts/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@3.0.0", "", { "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "tslib": "^1.11.1" } }, "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ=="],
@ -4775,16 +4633,6 @@
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/eslintrc/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@ -4843,8 +4691,6 @@
"@mintlify/models/axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="],
"@mintlify/openapi-parser/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="],
"@mintlify/prebuild/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
@ -4887,8 +4733,6 @@
"@mintlify/validation/zod-to-json-schema": ["zod-to-json-schema@3.20.4", "", { "peerDependencies": { "zod": "^3.20.0" } }, "sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg=="],
"@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
@ -4897,24 +4741,6 @@
"@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"@oclif/core/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="],
"@oclif/core/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"@oclif/core/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@oclif/core/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"@oclif/core/widest-line": ["widest-line@3.1.0", "", { "dependencies": { "string-width": "^4.0.0" } }, "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg=="],
"@oclif/core/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"@oclif/plugin-autocomplete/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="],
"@oclif/plugin-not-found/ansis": ["ansis@3.17.0", "", {}, "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg=="],
"@oclif/plugin-not-found/fast-levenshtein": ["fast-levenshtein@3.0.0", "", { "dependencies": { "fastest-levenshtein": "^1.0.7" } }, "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
@ -4963,20 +4789,10 @@
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@raycast/api/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
"@raycast/api/@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
"@raycast/api/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"@raycast/api/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"@react-router/dev/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
"@react-router/dev/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
"@repo/docs/supermemory": ["supermemory@3.10.0", "", {}, "sha512-xUTn6ElIIXwizj80ELDFgXjAcBpV9LtNz7kWl+PVQfVzFHM2lOguFHaDx6GoWuWW2GpnE3ikkKPCsKpvHFgqgg=="],
"@repo/docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@repo/web/@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
@ -4995,16 +4811,6 @@
"@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"@sindresorhus/slugify/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"@sindresorhus/transliterate/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="],
"@smithy/util-waiter/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="],
"@stoplight/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@stoplight/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"@stoplight/json/safe-stable-stringify": ["safe-stable-stringify@1.1.1", "", {}, "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw=="],
@ -5015,22 +4821,16 @@
"@stoplight/spectral-core/@stoplight/types": ["@stoplight/types@13.6.0", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ=="],
"@stoplight/spectral-core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@stoplight/spectral-core/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@stoplight/spectral-core/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@stoplight/spectral-functions/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@stoplight/spectral-functions/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"@stoplight/spectral-parsers/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@supermemory/ai-sdk/supermemory": ["supermemory@3.10.0", "", {}, "sha512-xUTn6ElIIXwizj80ELDFgXjAcBpV9LtNz7kWl+PVQfVzFHM2lOguFHaDx6GoWuWW2GpnE3ikkKPCsKpvHFgqgg=="],
"@supermemory/ai-sdk/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@supermemory/ai-sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
@ -5045,8 +4845,6 @@
"@supermemory/tools/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.65.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw=="],
"@supermemory/tools/supermemory": ["supermemory@3.10.0", "", {}, "sha512-xUTn6ElIIXwizj80ELDFgXjAcBpV9LtNz7kWl+PVQfVzFHM2lOguFHaDx6GoWuWW2GpnE3ikkKPCsKpvHFgqgg=="],
"@supermemory/tools/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@supermemory/tools/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
@ -5065,8 +4863,6 @@
"@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@vanilla-extract/integration/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
@ -5077,30 +4873,18 @@
"agents/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.23.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA=="],
"aggregate-error/clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="],
"aggregate-error/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
"ai-gateway-provider/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"ai-gateway-provider/ai": ["ai@4.3.19", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q=="],
"ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-keywords/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"alchemy/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.7", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250927.0" }, "optionalPeers": ["workerd"] }, "sha512-HtZuh166y0Olbj9bqqySckz0Rw9uHjggJeoGbDx5x+sgezBXlxO6tQSig2RZw5tgObF8mWI8zaPvQMkQZtAODw=="],
"alchemy/unenv": ["unenv@2.0.0-rc.21", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"atmn/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
@ -5127,6 +4911,8 @@
"chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
"chrome-launcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"chromium-bidi/urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="],
@ -5187,11 +4973,9 @@
"esast-util-from-js/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"espree/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-util-build-jsx/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
@ -5205,14 +4989,10 @@
"extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"favicons/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"find-process/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@ -5257,12 +5037,8 @@
"import-in-the-middle/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"ink/ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
"ink/cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
"ink/indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"ink/react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
"ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
@ -5279,12 +5055,6 @@
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"log-update/ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"mdast-util-frontmatter/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"memory-graph-playground/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
"memory-graph-playground/next": ["next@16.0.3", "", { "dependencies": { "@next/env": "16.0.3", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.3", "@next/swc-darwin-x64": "16.0.3", "@next/swc-linux-arm64-gnu": "16.0.3", "@next/swc-linux-arm64-musl": "16.0.3", "@next/swc-linux-x64-gnu": "16.0.3", "@next/swc-linux-x64-musl": "16.0.3", "@next/swc-win32-arm64-msvc": "16.0.3", "@next/swc-win32-x64-msvc": "16.0.3", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w=="],
@ -5343,6 +5113,8 @@
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
@ -5389,8 +5161,6 @@
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@ -5433,12 +5203,6 @@
"sucrase/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"supermemory/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
"supermemory/@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
"supermemory/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"supermemory-browser-extension/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"supermemory-mcp/supermemory": ["supermemory@4.0.0", "", {}, "sha512-xMN05PQ8kTv8DuXa2qf8h/9LaRI7v1Kz3Tutt97JPq+PzhGabKLv5YVbSgqHiPX5yXcSUBVBNYPPbhAQMF6GYQ=="],
@ -5461,8 +5225,6 @@
"unimport/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"unimport/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"unimport/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
@ -5483,14 +5245,10 @@
"web-ext-run/pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="],
"web-ext-run/strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="],
"webpack/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"webpack/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@ -5533,8 +5291,6 @@
"@aklinker1/rollup-plugin-visualizer/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@asyncapi/parser/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
@ -5619,6 +5375,8 @@
"@aws-sdk/client-cloudfront/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-sdk/client-cloudfront/@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="],
"@aws-sdk/client-cloudfront/fast-xml-parser/strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"@aws-sdk/client-sts/@aws-crypto/sha256-browser/@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@3.0.0", "", { "dependencies": { "tslib": "^1.11.1" } }, "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg=="],
@ -5767,10 +5525,6 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@ -5781,6 +5535,8 @@
"@mintlify/cli/inquirer/@inquirer/prompts": ["@inquirer/prompts@7.10.1", "", { "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", "@inquirer/editor": "^4.2.23", "@inquirer/expand": "^4.0.23", "@inquirer/input": "^4.3.1", "@inquirer/number": "^3.0.23", "@inquirer/password": "^4.0.23", "@inquirer/rawlist": "^4.1.11", "@inquirer/search": "^3.2.2", "@inquirer/select": "^4.4.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg=="],
"@mintlify/cli/inquirer/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"@mintlify/cli/inquirer/run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="],
"@mintlify/cli/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@ -5793,9 +5549,9 @@
"@mintlify/common/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"@mintlify/common/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"@mintlify/common/tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"@mintlify/common/tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"@mintlify/common/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
@ -5805,8 +5561,6 @@
"@mintlify/link-rot/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="],
"@mintlify/openapi-parser/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@mintlify/prebuild/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"@mintlify/prebuild/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
@ -5851,8 +5605,6 @@
"@mintlify/prebuild/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="],
"@mintlify/previewing/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@mintlify/previewing/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"@mintlify/previewing/express/body-parser": ["body-parser@1.20.1", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw=="],
@ -5899,8 +5651,6 @@
"@mintlify/scraping/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
@ -5927,16 +5677,6 @@
"@node-minify/core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
"@oclif/core/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"@oclif/core/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"@oclif/core/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@oclif/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@oclif/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@ -5975,60 +5715,6 @@
"@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@raycast/api/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"@raycast/api/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@raycast/api/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@raycast/api/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@raycast/api/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@raycast/api/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@raycast/api/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@raycast/api/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@raycast/api/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@raycast/api/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@raycast/api/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@raycast/api/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@raycast/api/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@raycast/api/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@raycast/api/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@raycast/api/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@raycast/api/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@raycast/api/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@raycast/api/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@raycast/api/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@raycast/api/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@raycast/api/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@raycast/api/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@raycast/api/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@raycast/api/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@raycast/api/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@raycast/api/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@repo/web/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
"@repo/web/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-v+LXXm8INLYAdxHnNMVAJ/B7k+Nejn5dCQMg/F8SRetB5dEQ4sbfimE+b6rawILJznnsy2fugUO1oFFXlUS5Yg=="],
@ -6037,14 +5723,8 @@
"@repo/web/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-4Dv/wiGZrvO6fI7P0yMLa4XZru0XW8LPibTObbkHBdweLUVGIze7aCfxxQeY44Uqcbl/h6/yBTkx2XmPtwf/Ow=="],
"@stoplight/better-ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@stoplight/spectral-core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@stoplight/spectral-functions/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
@ -6103,24 +5783,14 @@
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"agents/@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"agents/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"agents/@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"aggregate-error/clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"ai-gateway-provider/ai/@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
"ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -6151,10 +5821,6 @@
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"favicons/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
@ -6299,14 +5965,10 @@
"public-ip/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"supermemory/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"terser-webpack-plugin/terser/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"terser-webpack-plugin/terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
@ -6315,8 +5977,6 @@
"unimport/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"unplugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
@ -6371,8 +6031,6 @@
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
"webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
@ -6543,6 +6201,8 @@
"@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@mintlify/cli/inquirer/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"@mintlify/cli/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@mintlify/cli/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@ -6585,10 +6245,6 @@
"@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"@oclif/core/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@oclif/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@opennextjs/aws/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"@opennextjs/aws/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@ -6609,8 +6265,6 @@
"@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"agents/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"agents/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"agents/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],

View file

@ -158,6 +158,38 @@ export const apiSchema = createSchema({
input: MigrateMCPRequestSchema,
output: MigrateMCPResponseSchema,
},
"@post/documents/graph/viewport": {
input: z.object({
viewport: z.object({
minX: z.number(),
maxX: z.number(),
minY: z.number(),
maxY: z.number(),
}),
containerTags: z.array(z.string()).optional(),
timeFilter: z.object({
before: z.string().optional(),
}).optional(),
limit: z.number().optional(),
}),
output: z.object({
documents: z.array(z.any()),
edges: z.array(
z.object({
source: z.string(),
target: z.string(),
similarity: z.number(),
}),
),
viewport: z.object({
minX: z.number(),
maxX: z.number(),
minY: z.number(),
maxY: z.number(),
}),
timestamp: z.string(),
}),
},
"@get/documents/:id": {
output: z.any(),

View file

@ -43,10 +43,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
.then((org) => {
if (org.metadata?.isConsumer === true) {
console.log("Consumer organization:", org)
setOrg(org)
setOrg(org)
} else {
console.log("ALl orgs:", orgs)
const consumerOrg = orgs?.find((o) => o.metadata?.isConsumer === true)
const consumerOrg = orgs?.find(
(o) => o.metadata?.isConsumer === true,
)
if (consumerOrg) {
setActiveOrg(consumerOrg.slug)
}

View file

@ -84,11 +84,11 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const progress = Math.min(elapsed / duration, 1)
// Ease-out cubic easing for smooth deceleration
const eased = 1 - Math.pow(1 - progress, 3)
const eased = 1 - (1 - progress) ** 3
dimProgress.current = startDim + (targetDim - startDim) * eased
// Force re-render to update canvas during animation
forceRender(prev => prev + 1)
forceRender((prev) => prev + 1)
if (progress < 1) {
dimAnimationRef.current = requestAnimationFrame(animate)
@ -145,8 +145,10 @@ export const GraphCanvas = memo<GraphCanvasProps>(
// Only check nodes in the clicked cell (and neighboring cells for edge cases)
const cellsToCheck = [
cellKey,
`${cellX-1},${cellY}`, `${cellX+1},${cellY}`,
`${cellX},${cellY-1}`, `${cellX},${cellY+1}`,
`${cellX - 1},${cellY}`,
`${cellX + 1},${cellY}`,
`${cellX},${cellY - 1}`,
`${cellX},${cellY + 1}`,
]
// Check from top-most to bottom-most: memory nodes are drawn after documents
@ -360,7 +362,12 @@ export const GraphCanvas = memo<GraphCanvasProps>(
})
// Helper function to draw a single edge path
const drawEdgePath = (edge: typeof edges[0], sourceNode: GraphNode, targetNode: GraphNode, edgeShouldDim: boolean) => {
const drawEdgePath = (
edge: (typeof edges)[0],
sourceNode: GraphNode,
targetNode: GraphNode,
edgeShouldDim: boolean,
) => {
const sourceX = sourceNode.x * zoom + panX
const sourceY = sourceNode.y * zoom + panY
const targetX = targetNode.x * zoom + panX
@ -381,9 +388,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const dy = targetY - sourceY
const distance = Math.sqrt(dx * dx + dy * dy)
const controlOffset =
edge.edgeType === "doc-memory"
? 15
: Math.min(30, distance * 0.2)
edge.edgeType === "doc-memory" ? 15 : Math.min(30, distance * 0.2)
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
@ -398,7 +403,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}
// Smooth edge opacity: interpolate between full and 0.05 (dimmed)
const edgeDimOpacity = 1 - (dimProgress.current * 0.95)
const edgeDimOpacity = 1 - dimProgress.current * 0.95
// BATCH 1: Draw all doc-memory edges together
if (docMemoryEdges.length > 0) {
@ -417,7 +422,8 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
const edgeShouldDim = selectedNodeId !== null &&
const edgeShouldDim =
selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.9
@ -444,10 +450,13 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
const edgeShouldDim = selectedNodeId !== null &&
const edgeShouldDim =
selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : Math.max(0, edge.similarity * 0.5)
const opacity = edgeShouldDim
? edgeDimOpacity
: Math.max(0, edge.similarity * 0.5)
const lineWidth = Math.max(1, edge.similarity * 2)
// Set color based on similarity strength
@ -480,7 +489,8 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
const edgeShouldDim = selectedNodeId !== null &&
const edgeShouldDim =
selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.8
@ -568,7 +578,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const isSelected = selectedNodeId === node.id
const shouldDim = selectedNodeId !== null && !isSelected
// Smooth opacity: interpolate between 1 (full) and 0.1 (dimmed) based on animation progress
const nodeOpacity = shouldDim ? 1 - (dimProgress.current * 0.9) : 1
const nodeOpacity = shouldDim ? 1 - dimProgress.current * 0.9 : 1
const isHighlightedDocument = (() => {
if (node.type !== "document" || highlightSet.size === 0) return false
const doc = node.data as DocumentWithMemories
@ -710,7 +720,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const radius = nodeSize / 2
ctx.fillStyle = fillColor
ctx.globalAlpha = shouldDim ? nodeOpacity : (isLatest ? 1 : 0.4)
ctx.globalAlpha = shouldDim ? nodeOpacity : isLatest ? 1 : 0.4
ctx.strokeStyle = borderColor
ctx.lineWidth = isDragging ? 3 : isHovered ? 2 : 1.5
@ -833,7 +843,17 @@ export const GraphCanvas = memo<GraphCanvasProps>(
})
ctx.globalAlpha = 1
}, [nodes, edges, panX, panY, zoom, width, height, highlightDocumentIds, nodeMap])
}, [
nodes,
edges,
panX,
panY,
zoom,
width,
height,
highlightDocumentIds,
nodeMap,
])
// Hybrid rendering: continuous when simulation active, change-based when idle
const lastRenderParams = useRef<number>(0)
@ -857,9 +877,16 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}, 0)
// Combine all factors into a single number
return positionHash ^ edges.length ^
Math.round(panX) ^ Math.round(panY) ^
Math.round(zoom * 100) ^ width ^ height ^ highlightHash
return (
positionHash ^
edges.length ^
Math.round(panX) ^
Math.round(panY) ^
Math.round(zoom * 100) ^
width ^
height ^
highlightHash
)
}, [
nodes,
edges.length,
@ -962,13 +989,10 @@ export const GraphCanvas = memo<GraphCanvasProps>(
// Calculate effective DPR that keeps us within safe limits
// Prevent division by zero by checking for valid dimensions
const maxDpr = width > 0 && height > 0
? Math.min(
MAX_CANVAS_SIZE / width,
MAX_CANVAS_SIZE / height,
dpr
)
: dpr
const maxDpr =
width > 0 && height > 0
? Math.min(MAX_CANVAS_SIZE / width, MAX_CANVAS_SIZE / height, dpr)
: dpr
// upscale backing store with clamped dimensions
canvas.style.width = `${width}px`
@ -1026,4 +1050,4 @@ export const GraphCanvas = memo<GraphCanvasProps>(
},
)
GraphCanvas.displayName = "GraphCanvas"
GraphCanvas.displayName = "GraphCanvas"

View file

@ -2,7 +2,14 @@
import { GlassMenuEffect } from "@/ui/glass-effect"
import { AnimatePresence } from "motion/react"
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"
import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react"
import { GraphCanvas } from "./graph-canvas"
import { useGraphData } from "@/hooks/use-graph-data"
import { useGraphInteractions } from "@/hooks/use-graph-interactions"
@ -426,8 +433,10 @@ export const MemoryGraph = ({
// Calculate node dimensions to position popover with proper gap
const nodeSize = selectedNodeData.size * zoom
const nodeWidth = selectedNodeData.type === "document" ? nodeSize * 1.4 : nodeSize
const nodeHeight = selectedNodeData.type === "document" ? nodeSize * 0.9 : nodeSize
const nodeWidth =
selectedNodeData.type === "document" ? nodeSize * 1.4 : nodeSize
const nodeHeight =
selectedNodeData.type === "document" ? nodeSize * 0.9 : nodeSize
const gap = 20 // Gap between node and popover
// Smart positioning: flip to other side if would go off-screen
@ -457,7 +466,14 @@ export const MemoryGraph = ({
}
return { x: popoverX, y: popoverY }
}, [selectedNodeData, zoom, panX, panY, containerSize.width, containerSize.height])
}, [
selectedNodeData,
zoom,
panX,
panY,
containerSize.width,
containerSize.height,
])
// Viewport-based loading: load more when most documents are visible (optional)
const checkAndLoadMore = useCallback(() => {
@ -564,7 +580,14 @@ export const MemoryGraph = ({
containerSizeRef.current = containerSize
onSlideshowNodeChangeRef.current = onSlideshowNodeChange
forceSimulationRef.current = forceSimulation
}, [nodes, handleNodeClick, centerViewportOn, containerSize, onSlideshowNodeChange, forceSimulation])
}, [
nodes,
handleNodeClick,
centerViewportOn,
containerSize,
onSlideshowNodeChange,
forceSimulation,
])
useEffect(() => {
// Clear any existing interval and timeout when isSlideshowActive changes
@ -731,7 +754,7 @@ export const MemoryGraph = ({
height={containerSize.height}
nodes={nodes}
highlightDocumentIds={highlightsVisible ? highlightDocumentIds : []}
isSimulationActive={forceSimulation.isActive()}
isSimulationActive={forceSimulation.isActive()}
onDoubleClick={handleDoubleClick}
onNodeClick={handleNodeClickWithPhysics}
onNodeDragEnd={handleNodeDragEndWithPhysics}

View file

@ -17,7 +17,7 @@ const navButtonBase = style({
backgroundColor: "rgba(0, 0, 0, 0.2)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
border: `1px solid rgba(255, 255, 255, 0.1)`,
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: themeContract.radii.lg,
padding: themeContract.space[2],
color: "rgba(255, 255, 255, 0.7)",

View file

@ -68,7 +68,7 @@ const getDocumentIcon = (type: string) => {
return <PDF {...iconProps} />
default:
{
/*@ts-ignore */
/*@ts-expect-error */
}
return <FileText {...iconProps} />
}
@ -109,7 +109,7 @@ export const NodeDetailPanel = memo(function NodeDetailPanel({
{isDocument ? (
getDocumentIcon((data as DocumentWithMemories).type ?? "")
) : (
// @ts-ignore
// @ts-expect-error
<Brain className={styles.headerIconMemory} />
)}
<HeadingH3Bold>{isDocument ? "Document" : "Memory"}</HeadingH3Bold>

View file

@ -55,7 +55,11 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
return (
<>
{/* Invisible backdrop to catch clicks outside */}
<div onClick={handleBackdropClick} className={backdropClassName} style={backdropStyle} />
<div
onClick={handleBackdropClick}
className={backdropClassName}
style={backdropStyle}
/>
{/* Popover content */}
<div
@ -72,16 +76,24 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Header */}
<div className={styles.header}>
<div className={styles.headerTitle}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.headerIcon}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={styles.headerIcon}
>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
<h3 className={styles.title}>
Document
</h3>
<h3 className={styles.title}>Document</h3>
</div>
<button
type="button"
@ -96,9 +108,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div className={styles.sectionsContainer}>
{/* Title */}
<div>
<div className={styles.fieldLabel}>
Title
</div>
<div className={styles.fieldLabel}>Title</div>
<p className={styles.fieldValue}>
{(node.data as any).title || "Untitled Document"}
</p>
@ -107,9 +117,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Summary - truncated to 2 lines */}
{(node.data as any).summary && (
<div>
<div className={styles.fieldLabel}>
Summary
</div>
<div className={styles.fieldLabel}>Summary</div>
<p className={styles.summaryValue}>
{(node.data as any).summary}
</p>
@ -118,9 +126,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Type */}
<div>
<div className={styles.fieldLabel}>
Type
</div>
<div className={styles.fieldLabel}>Type</div>
<p className={styles.fieldValue}>
{(node.data as any).type || "Document"}
</p>
@ -128,9 +134,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Memory Count */}
<div>
<div className={styles.fieldLabel}>
Memory Count
</div>
<div className={styles.fieldLabel}>Memory Count</div>
<p className={styles.fieldValue}>
{(node.data as any).memoryEntries?.length || 0} memories
</p>
@ -139,9 +143,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* URL */}
{((node.data as any).url || (node.data as any).customId) && (
<div>
<div className={styles.fieldLabel}>
URL
</div>
<div className={styles.fieldLabel}>URL</div>
<a
href={(() => {
const doc = node.data as any
@ -160,10 +162,19 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
rel="noopener noreferrer"
className={styles.link}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" y1="14" x2="21" y2="3" />
</svg>
View Document
</a>
@ -173,20 +184,42 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Footer with metadata */}
<div className={styles.footer}>
<div className={styles.footerItem}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<span>{new Date((node.data as any).createdAt).toLocaleDateString()}</span>
<span>
{new Date(
(node.data as any).createdAt,
).toLocaleDateString()}
</span>
</div>
<div className={styles.footerItemId}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="4" y1="9" x2="20" y2="9"></line>
<line x1="4" y1="15" x2="20" y2="15"></line>
<line x1="10" y1="3" x2="8" y2="21"></line>
<line x1="16" y1="3" x2="14" y2="21"></line>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="4" y1="9" x2="20" y2="9" />
<line x1="4" y1="15" x2="20" y2="15" />
<line x1="10" y1="3" x2="8" y2="21" />
<line x1="16" y1="3" x2="14" y2="21" />
</svg>
<span className={styles.idText}>{node.id}</span>
</div>
@ -199,13 +232,21 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Header */}
<div className={styles.header}>
<div className={styles.headerTitle}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.headerIconMemory}>
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"></path>
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"></path>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={styles.headerIconMemory}
>
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
</svg>
<h3 className={styles.title}>
Memory
</h3>
<h3 className={styles.title}>Memory</h3>
</div>
<button
type="button"
@ -220,31 +261,31 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div className={styles.sectionsContainer}>
{/* Memory content */}
<div>
<div className={styles.fieldLabel}>
Memory
</div>
<div className={styles.fieldLabel}>Memory</div>
<p className={styles.fieldValue}>
{(node.data as any).memory || (node.data as any).content || "No content"}
{(node.data as any).memory ||
(node.data as any).content ||
"No content"}
</p>
{(node.data as any).isForgotten && (
<div className={styles.forgottenBadge}>
Forgotten
</div>
<div className={styles.forgottenBadge}>Forgotten</div>
)}
{/* Expires (inline with memory if exists) */}
{(node.data as any).forgetAfter && (
<p className={styles.expiresText}>
Expires: {new Date((node.data as any).forgetAfter).toLocaleDateString()}
{(node.data as any).forgetReason && ` - ${(node.data as any).forgetReason}`}
Expires:{" "}
{new Date(
(node.data as any).forgetAfter,
).toLocaleDateString()}
{(node.data as any).forgetReason &&
` - ${(node.data as any).forgetReason}`}
</p>
)}
</div>
{/* Space */}
<div>
<div className={styles.fieldLabel}>
Space
</div>
<div className={styles.fieldLabel}>Space</div>
<p className={styles.fieldValue}>
{(node.data as any).spaceId || "Default"}
</p>
@ -253,20 +294,42 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Footer with metadata */}
<div className={styles.footer}>
<div className={styles.footerItem}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
<span>{new Date((node.data as any).createdAt).toLocaleDateString()}</span>
<span>
{new Date(
(node.data as any).createdAt,
).toLocaleDateString()}
</span>
</div>
<div className={styles.footerItemId}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="4" y1="9" x2="20" y2="9"></line>
<line x1="4" y1="15" x2="20" y2="15"></line>
<line x1="10" y1="3" x2="8" y2="21"></line>
<line x1="16" y1="3" x2="14" y2="21"></line>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="4" y1="9" x2="20" y2="9" />
<line x1="4" y1="15" x2="20" y2="15" />
<line x1="10" y1="3" x2="8" y2="21" />
<line x1="16" y1="3" x2="14" y2="21" />
</svg>
<span className={styles.idText}>{node.id}</span>
</div>

Some files were not shown because too many files have changed in this diff Show more