mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
new graph
This commit is contained in:
parent
d0d7d28a8d
commit
475bdf63ff
126 changed files with 8281 additions and 2761 deletions
|
|
@ -209,7 +209,6 @@ function addSupermemoryButtonToMemoriesDialog() {
|
|||
|
||||
if (memoriesDialog.querySelector("#supermemory-save-button")) return
|
||||
|
||||
|
||||
const deleteAllContainer = memoriesDialog.querySelector(
|
||||
".flex.items-center.gap-0\\.5",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ async function handleAllBookmarksImportClick() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async function showAllBookmarksProjectModal(
|
||||
projects: Array<{ id: string; name: string; containerTag: string }>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
|
|
|||
56
apps/web/app/(navigation)/graph/page.tsx
Normal file
56
apps/web/app/(navigation)/graph/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ export default function BillingPage() {
|
|||
<BillingView />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ export default function IntegrationsPage() {
|
|||
<IntegrationsView />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ export default function ProfilePage() {
|
|||
<ProfileView />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
67
apps/web/components/graph/constants.ts
Normal file
67
apps/web/components/graph/constants.ts
Normal 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
|
||||
204
apps/web/components/graph/document-icons.ts
Normal file
204
apps/web/components/graph/document-icons.ts
Normal 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()
|
||||
}
|
||||
760
apps/web/components/graph/graph-canvas.tsx
Normal file
760
apps/web/components/graph/graph-canvas.tsx
Normal 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"
|
||||
443
apps/web/components/graph/graph.tsx
Normal file
443
apps/web/components/graph/graph.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
10
apps/web/components/graph/index.ts
Normal file
10
apps/web/components/graph/index.ts
Normal 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"
|
||||
178
apps/web/components/graph/legend.tsx
Normal file
178
apps/web/components/graph/legend.tsx
Normal 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"
|
||||
28
apps/web/components/graph/loading-indicator.tsx
Normal file
28
apps/web/components/graph/loading-indicator.tsx
Normal 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"
|
||||
87
apps/web/components/graph/navigation-controls.tsx
Normal file
87
apps/web/components/graph/navigation-controls.tsx
Normal 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"
|
||||
248
apps/web/components/graph/node-popover.tsx
Normal file
248
apps/web/components/graph/node-popover.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
})
|
||||
65
apps/web/components/graph/similarity.ts
Normal file
65
apps/web/components/graph/similarity.ts
Normal 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}%)`
|
||||
}
|
||||
96
apps/web/components/graph/types.ts
Normal file
96
apps/web/components/graph/types.ts
Normal 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
|
||||
}
|
||||
40
apps/web/components/graph/use-graph-data.ts
Normal file
40
apps/web/components/graph/use-graph-data.ts
Normal 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 }
|
||||
}
|
||||
470
apps/web/components/graph/use-graph-interactions.ts
Normal file
470
apps/web/components/graph/use-graph-interactions.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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\./, "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
73
apps/web/components/viewport-graph/constants.ts
Normal file
73
apps/web/components/viewport-graph/constants.ts
Normal 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",
|
||||
},
|
||||
}
|
||||
209
apps/web/components/viewport-graph/document-icons.ts
Normal file
209
apps/web/components/viewport-graph/document-icons.ts
Normal 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()
|
||||
}
|
||||
5
apps/web/components/viewport-graph/index.ts
Normal file
5
apps/web/components/viewport-graph/index.ts
Normal 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"
|
||||
28
apps/web/components/viewport-graph/loading-indicator.tsx
Normal file
28
apps/web/components/viewport-graph/loading-indicator.tsx
Normal 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"
|
||||
65
apps/web/components/viewport-graph/navigation-controls.tsx
Normal file
65
apps/web/components/viewport-graph/navigation-controls.tsx
Normal 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"
|
||||
187
apps/web/components/viewport-graph/node-popover.tsx
Normal file
187
apps/web/components/viewport-graph/node-popover.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
})
|
||||
675
apps/web/components/viewport-graph/viewport-canvas.tsx
Normal file
675
apps/web/components/viewport-graph/viewport-canvas.tsx
Normal 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"
|
||||
290
apps/web/components/viewport-graph/viewport-graph.tsx
Normal file
290
apps/web/components/viewport-graph/viewport-graph.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
Copy,
|
||||
RotateCcw,
|
||||
X,
|
||||
Square
|
||||
Square,
|
||||
} from "lucide-react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
"use client"
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useProject } from "@/stores";
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useMemo } from "react"
|
||||
import { useProject } from "@/stores"
|
||||
|
||||
/**
|
||||
* Returns the display name of the currently selected project.
|
||||
|
|
@ -10,17 +10,17 @@ import { useProject } from "@/stores";
|
|||
* hasn’t been fetched yet.
|
||||
*/
|
||||
export function useProjectName() {
|
||||
const { selectedProject } = useProject();
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject } = useProject()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// This query is populated by ProjectsView – we just read from the cache.
|
||||
const projects = queryClient.getQueryData(["projects"]) as
|
||||
| Array<{ name: string; containerTag: string }>
|
||||
| undefined;
|
||||
| undefined
|
||||
|
||||
return useMemo(() => {
|
||||
if (selectedProject === "sm_project_default") return "Default Project";
|
||||
const found = projects?.find((p) => p.containerTag === selectedProject);
|
||||
return found?.name ?? selectedProject;
|
||||
}, [projects, selectedProject]);
|
||||
if (selectedProject === "sm_project_default") return "Default Project"
|
||||
const found = projects?.find((p) => p.containerTag === selectedProject)
|
||||
return found?.name ?? selectedProject
|
||||
}, [projects, selectedProject])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
314
apps/web/hooks/use-timeline-stream.ts
Normal file
314
apps/web/hooks/use-timeline-stream.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
284
apps/web/hooks/use-viewport-graph.ts
Normal file
284
apps/web/hooks/use-viewport-graph.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
534
apps/web/hooks/use-viewport-interactions.ts
Normal file
534
apps/web/hooks/use-viewport-interactions.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
108
apps/web/lib/viewport-graph-types.ts
Normal file
108
apps/web/lib/viewport-graph-types.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
406
bun.lock
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue