mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
feat: mobile responsive, lint formats, toast, render issue fix (#688)
- Mobile responsive - new toast design - web document render issue fix - posthog analytics - ui improvements
This commit is contained in:
parent
731808be68
commit
1423bd7004
119 changed files with 3966 additions and 3029 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,17 +257,20 @@ async function showOnboardingToast() {
|
|||
const icon = document.createElement("img")
|
||||
icon.src = iconUrl
|
||||
icon.alt = "Supermemory"
|
||||
icon.style.cssText = "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;"
|
||||
icon.style.cssText =
|
||||
"width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;"
|
||||
|
||||
const textContainer = document.createElement("div")
|
||||
textContainer.style.cssText = "display: flex; flex-direction: column; gap: 4px; flex: 1;"
|
||||
textContainer.style.cssText =
|
||||
"display: flex; flex-direction: column; gap: 4px; flex: 1;"
|
||||
|
||||
const title = document.createElement("span")
|
||||
title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;"
|
||||
title.textContent = "Import X/Twitter Bookmarks"
|
||||
|
||||
const description = document.createElement("span")
|
||||
description.style.cssText = "font-size: 13px; color: #6b7280; line-height: 1.4;"
|
||||
description.style.cssText =
|
||||
"font-size: 13px; color: #6b7280; line-height: 1.4;"
|
||||
description.textContent =
|
||||
"You can import all your Twitter bookmarks to Supermemory with one click."
|
||||
|
||||
|
|
@ -362,10 +365,7 @@ async function showOnboardingToast() {
|
|||
learnMoreButton.style.backgroundColor = "transparent"
|
||||
})
|
||||
learnMoreButton.addEventListener("click", () => {
|
||||
window.open(
|
||||
"https://docs.supermemory.ai/connectors/twitter",
|
||||
"_blank",
|
||||
)
|
||||
window.open("https://docs.supermemory.ai/connectors/twitter", "_blank")
|
||||
})
|
||||
|
||||
buttonsContainer.appendChild(importButton)
|
||||
|
|
@ -377,7 +377,10 @@ async function showOnboardingToast() {
|
|||
progressBarContainer.setAttribute("aria-valuemin", "0")
|
||||
progressBarContainer.setAttribute("aria-valuemax", "100")
|
||||
progressBarContainer.setAttribute("aria-valuenow", "0")
|
||||
progressBarContainer.setAttribute("aria-label", "Onboarding toast auto-dismiss progress")
|
||||
progressBarContainer.setAttribute(
|
||||
"aria-label",
|
||||
"Onboarding toast auto-dismiss progress",
|
||||
)
|
||||
progressBarContainer.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
@ -394,7 +397,7 @@ async function showOnboardingToast() {
|
|||
transform-origin: left;
|
||||
animation: smProgressGrow ${duration}ms linear forwards;
|
||||
`
|
||||
|
||||
|
||||
// Update progress bar ARIA value as animation progresses
|
||||
const startTime = Date.now()
|
||||
const updateProgress = () => {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,10 @@ export async function validateAuthToken(): Promise<boolean> {
|
|||
/**
|
||||
* Get user data from storage
|
||||
*/
|
||||
export async function getUserData(): Promise<{ email?: string; name?: string } | null> {
|
||||
export async function getUserData(): Promise<{
|
||||
email?: string
|
||||
name?: string
|
||||
} | null> {
|
||||
try {
|
||||
return (await userData.getValue()) || null
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ export const ELEMENT_IDS = {
|
|||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
TWITTER_BOOKMARKS_ONBOARDING_SEEN: "sm_twitter_bookmarks_onboarding_seen",
|
||||
TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL: "sm_twitter_bookmarks_import_intent_until",
|
||||
TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL:
|
||||
"sm_twitter_bookmarks_import_intent_until",
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.dark img[src*="openai.svg"],
|
||||
.dark img[src*="pipecat.svg"],
|
||||
.dark img[src*="supermemory.svg"] {
|
||||
filter: invert(1);
|
||||
filter: invert(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,9 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
|
|||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "An unexpected error occurred"
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An unexpected error occurred"
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ export async function POST(req: Request) {
|
|||
},
|
||||
{
|
||||
type: "x",
|
||||
includedXHandles: [lowerUrl.replace("https://x.com/", "").replace("https://twitter.com/", "")],
|
||||
includedXHandles: [
|
||||
lowerUrl
|
||||
.replace("https://x.com/", "")
|
||||
.replace("https://twitter.com/", ""),
|
||||
],
|
||||
postFavoriteCount: 10,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { PostHogProvider } from "@lib/posthog"
|
|||
import { QueryProvider } from "../components/query-client"
|
||||
import { AutumnProvider } from "autumn-js/react"
|
||||
import { Suspense } from "react"
|
||||
import { Toaster } from "sonner"
|
||||
import { Toaster } from "@ui/components/sonner"
|
||||
import { MobilePanelProvider } from "@/lib/mobile-panel-context"
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app"
|
||||
import { ThemeProvider } from "@/lib/theme-provider"
|
||||
|
|
@ -66,7 +66,7 @@ export default function RootLayout({
|
|||
<ErrorTrackingProvider>
|
||||
<NuqsAdapter>
|
||||
<Suspense>{children}</Suspense>
|
||||
<Toaster richColors />
|
||||
<Toaster />
|
||||
</NuqsAdapter>
|
||||
</ErrorTrackingProvider>
|
||||
</PostHogProvider>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { IntegrationsStep } from "@/components/new/onboarding/setup/integrations
|
|||
import { SetupHeader } from "@/components/new/onboarding/setup/header"
|
||||
import { ChatSidebar } from "@/components/new/onboarding/setup/chat-sidebar"
|
||||
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
|
||||
import { useSetupContext, type SetupStep } from "./layout"
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ function StepNotFound({ goToStep }: { goToStep: (step: SetupStep) => void }) {
|
|||
|
||||
export default function SetupPage() {
|
||||
const { memoryFormData, currentStep, goToStep } = useSetupContext()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const renderStep = () => {
|
||||
switch (currentStep) {
|
||||
|
|
@ -52,17 +54,21 @@ export default function SetupPage() {
|
|||
|
||||
<main className="relative min-h-screen">
|
||||
<div className="relative z-10">
|
||||
<div className="flex flex-row h-[calc(100vh-90px)] relative">
|
||||
<div className="flex-1 flex flex-col items-center justify-start p-8">
|
||||
<div className="flex flex-col lg:flex-row h-[calc(100vh-90px)] relative">
|
||||
<div className="flex-1 flex flex-col items-center justify-start p-4 md:p-8">
|
||||
<AnimatePresence mode="wait">{renderStep()}</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
<ChatSidebar formData={memoryFormData} />
|
||||
</AnimatePresence>
|
||||
{!isMobile && (
|
||||
<AnimatePresence mode="popLayout">
|
||||
<ChatSidebar formData={memoryFormData} />
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{isMobile && <ChatSidebar formData={memoryFormData} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ export default function WelcomePage() {
|
|||
localStorage.setItem("username", name)
|
||||
if (name.trim()) {
|
||||
setIsSubmitting(true)
|
||||
|
||||
|
||||
try {
|
||||
await authClient.updateUser({ displayUsername: name.trim() })
|
||||
} catch (error) {
|
||||
console.error("Failed to update displayUsername:", error)
|
||||
}
|
||||
|
||||
|
||||
goToStep("greeting")
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,32 +10,45 @@ import { MCPModal } from "@/components/new/mcp-modal"
|
|||
import { HotkeysProvider } from "react-hotkeys-hook"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { AnimatePresence } from "motion/react"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
export default function NewPage() {
|
||||
const isMobile = useIsMobile()
|
||||
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
|
||||
const [isMCPModalOpen, setIsMCPModalOpen] = useState(false)
|
||||
useHotkeys("c", () => setIsAddDocumentOpen(true))
|
||||
const [isChatOpen, setIsChatOpen] = useState(true)
|
||||
useHotkeys("c", () => {
|
||||
analytics.addDocumentModalOpened()
|
||||
setIsAddDocumentOpen(true)
|
||||
})
|
||||
const [isChatOpen, setIsChatOpen] = useState(!isMobile)
|
||||
|
||||
return (
|
||||
<HotkeysProvider>
|
||||
<div className="bg-black">
|
||||
<div className="bg-black min-h-screen">
|
||||
<AnimatedGradientBackground
|
||||
topPosition="15%"
|
||||
animateFromBottom={false}
|
||||
/>
|
||||
<Header
|
||||
onAddMemory={() => setIsAddDocumentOpen(true)}
|
||||
onOpenMCP={() => setIsMCPModalOpen(true)}
|
||||
onAddMemory={() => {
|
||||
analytics.addDocumentModalOpened()
|
||||
setIsAddDocumentOpen(true)
|
||||
}}
|
||||
onOpenMCP={() => {
|
||||
analytics.mcpModalOpened()
|
||||
setIsMCPModalOpen(true)
|
||||
}}
|
||||
onOpenChat={() => setIsChatOpen(true)}
|
||||
/>
|
||||
<main
|
||||
key={`main-container-${isChatOpen}`}
|
||||
className="z-10 flex flex-row relative"
|
||||
className="z-10 flex flex-col md:flex-row relative"
|
||||
>
|
||||
<div className="flex-1 p-6 pr-0">
|
||||
<div className="flex-1 p-4 md:p-6 md:pr-0">
|
||||
<MemoriesGrid isChatOpen={isChatOpen} />
|
||||
</div>
|
||||
<div className="sticky top-0 h-screen">
|
||||
<div className="hidden md:block md:sticky md:top-0 md:h-screen">
|
||||
<AnimatePresence mode="popLayout">
|
||||
<ChatSidebar
|
||||
isChatOpen={isChatOpen}
|
||||
|
|
@ -45,6 +58,10 @@ export default function NewPage() {
|
|||
</div>
|
||||
</main>
|
||||
|
||||
{isMobile && (
|
||||
<ChatSidebar isChatOpen={isChatOpen} setIsChatOpen={setIsChatOpen} />
|
||||
)}
|
||||
|
||||
<AddDocumentModal
|
||||
isOpen={isAddDocumentOpen}
|
||||
onClose={() => setIsAddDocumentOpen(false)}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Integrations from "@/components/new/settings/integrations"
|
|||
import ConnectionsMCP from "@/components/new/settings/connections-mcp"
|
||||
import Support from "@/components/new/settings/support"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
|
||||
const TABS = ["account", "integrations", "connections", "support"] as const
|
||||
type SettingsTab = (typeof TABS)[number]
|
||||
|
|
@ -148,6 +149,7 @@ export default function SettingsPage() {
|
|||
const [activeTab, setActiveTab] = useState<SettingsTab>("account")
|
||||
const hasInitialized = useRef(false)
|
||||
const router = useRouter()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) return
|
||||
|
|
@ -174,7 +176,7 @@ export default function SettingsPage() {
|
|||
}, [])
|
||||
return (
|
||||
<div className="h-screen flex flex-col overflow-hidden">
|
||||
<header className="flex justify-between items-center px-6 py-3 shrink-0">
|
||||
<header className="flex justify-between items-center px-4 md:px-6 py-3 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/new")}
|
||||
|
|
@ -191,55 +193,77 @@ export default function SettingsPage() {
|
|||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="max-w-2xl mx-auto space-x-12 flex justify-center pt-4 flex-1 min-h-0">
|
||||
<div className="min-w-xs">
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: 1,
|
||||
padding: 48,
|
||||
paddingTop: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
ease: "easeOut",
|
||||
delay: 0.2,
|
||||
}}
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<NovaOrb size={175} className="blur-[3px]!" />
|
||||
<UserSupermemory name={user?.name ?? ""} />
|
||||
</motion.div>
|
||||
<nav className={cn("flex flex-col gap-2", dmSansClassName())}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.hash = item.id
|
||||
setActiveTab(item.id)
|
||||
<main className="flex-1 min-h-0 overflow-y-auto md:overflow-hidden">
|
||||
<div className="flex flex-col md:flex-row md:justify-center gap-4 md:gap-8 lg:gap-12 px-4 md:px-6 pt-4 pb-6 md:h-full">
|
||||
<div className="w-full md:w-auto md:min-w-[280px] shrink-0">
|
||||
{!isMobile && (
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: 1,
|
||||
padding: 48,
|
||||
paddingTop: 0,
|
||||
}}
|
||||
className={`text-left p-4 rounded-xl transition-colors flex items-start gap-3 ${
|
||||
activeTab === item.id
|
||||
? "bg-[#14161A] text-white shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
|
||||
: "text-white/60 hover:text-white hover:bg-[#14161A] hover:shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
|
||||
}`}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
ease: "easeOut",
|
||||
delay: 0.2,
|
||||
}}
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<span className="mt-0.5 shrink-0">{item.icon}</span>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-medium">{item.label}</span>
|
||||
<span className="text-sm text-white/50">
|
||||
{item.description}
|
||||
<NovaOrb size={175} className="blur-[3px]!" />
|
||||
<UserSupermemory name={user?.name ?? ""} />
|
||||
</motion.div>
|
||||
)}
|
||||
<nav
|
||||
className={cn(
|
||||
"flex gap-2",
|
||||
isMobile
|
||||
? "flex-row overflow-x-auto pb-2 scrollbar-thin"
|
||||
: "flex-col",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.hash = item.id
|
||||
setActiveTab(item.id)
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-xl transition-colors flex items-start gap-3 shrink-0",
|
||||
isMobile ? "px-3 py-2 text-sm" : "text-left p-4",
|
||||
activeTab === item.id
|
||||
? "bg-[#14161A] text-white shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
|
||||
: "text-white/60 hover:text-white hover:bg-[#14161A] hover:shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
|
||||
)}
|
||||
>
|
||||
<span className={cn("shrink-0", !isMobile && "mt-0.5")}>
|
||||
{item.icon}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto min-w-2xl [scrollbar-gutter:stable] pr-[17px]">
|
||||
{activeTab === "account" && <Account />}
|
||||
{activeTab === "integrations" && <Integrations />}
|
||||
{activeTab === "connections" && <ConnectionsMCP />}
|
||||
{activeTab === "support" && <Support />}
|
||||
{isMobile ? (
|
||||
<span className="font-medium whitespace-nowrap">
|
||||
{item.label}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-medium">{item.label}</span>
|
||||
<span className="text-sm text-white/50">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-4 md:overflow-y-auto md:max-w-2xl [scrollbar-gutter:stable] md:pr-[17px]">
|
||||
{activeTab === "account" && <Account />}
|
||||
{activeTab === "integrations" && <Integrations />}
|
||||
{activeTab === "connections" && <ConnectionsMCP />}
|
||||
{activeTab === "support" && <Support />}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { useDocumentMutations } from "../../../hooks/use-document-mutations"
|
|||
import { useCustomer } from "autumn-js/react"
|
||||
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
|
||||
import { SpaceSelector } from "../space-selector"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
|
||||
type TabType = "note" | "link" | "file" | "connect"
|
||||
|
||||
|
|
@ -30,11 +31,16 @@ export function AddDocumentModal({
|
|||
onClose,
|
||||
defaultTab,
|
||||
}: AddDocumentModalProps) {
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"w-[80%]! max-w-[1000px]! h-[80%]! max-h-[800px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-3 rounded-[22px]",
|
||||
"border-none bg-[#1B1F24] flex flex-col p-3 md:p-4 gap-3",
|
||||
isMobile
|
||||
? "w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-xl"
|
||||
: "w-[80%]! max-w-[1000px]! h-[80%]! max-h-[800px]! rounded-[22px]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -93,6 +99,7 @@ export function AddDocument({
|
|||
onClose: () => void
|
||||
isOpen?: boolean
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [activeTab, setActiveTab] = useState<TabType>(defaultTab ?? "note")
|
||||
const { selectedProject: globalSelectedProject } = useProject()
|
||||
const [localSelectedProject, setLocalSelectedProject] = useState<string>(
|
||||
|
|
@ -208,9 +215,21 @@ export function AddDocument({
|
|||
noteMutation.isPending || linkMutation.isPending || fileMutation.isPending
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-row text-white space-x-5">
|
||||
<div className="w-1/3 flex flex-col justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="h-full flex flex-col md:flex-row text-white md:space-x-5 space-y-3 md:space-y-0">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col justify-between",
|
||||
isMobile ? "w-full" : "w-1/3",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-1",
|
||||
isMobile
|
||||
? "flex-row overflow-x-auto pb-2 scrollbar-thin"
|
||||
: "flex-col",
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<TabButton
|
||||
key={tab.id}
|
||||
|
|
@ -220,47 +239,55 @@ export function AddDocument({
|
|||
title={tab.title}
|
||||
description={tab.description}
|
||||
isPro={tab.isPro}
|
||||
compact={isMobile}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="memories-counter"
|
||||
className="bg-[#1B1F24] rounded-2xl p-4 mr-4"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className={cn(
|
||||
"text-white text-[16px] font-medium",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Memories
|
||||
</span>
|
||||
<span className={cn("text-[#737373] text-sm", dmSansClassName())}>
|
||||
{isLoadingMemories
|
||||
? "…"
|
||||
: hasProProduct
|
||||
? "Unlimited"
|
||||
: `${memoriesUsed}/${memoriesLimit}`}
|
||||
</span>
|
||||
</div>
|
||||
{!hasProProduct && (
|
||||
<div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
|
||||
<div
|
||||
className="h-full bg-[#2261CA] rounded-full"
|
||||
style={{ width: `${usagePercent}%` }}
|
||||
/>
|
||||
{!isMobile && (
|
||||
<div
|
||||
data-testid="memories-counter"
|
||||
className="bg-[#1B1F24] rounded-2xl p-4 mr-4"
|
||||
style={{
|
||||
boxShadow:
|
||||
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className={cn(
|
||||
"text-white text-[16px] font-medium",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Memories
|
||||
</span>
|
||||
<span className={cn("text-[#737373] text-sm", dmSansClassName())}>
|
||||
{isLoadingMemories
|
||||
? "…"
|
||||
: hasProProduct
|
||||
? "Unlimited"
|
||||
: `${memoriesUsed}/${memoriesLimit}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!hasProProduct && (
|
||||
<div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
|
||||
<div
|
||||
className="h-full bg-[#2261CA] rounded-full"
|
||||
style={{ width: `${usagePercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-2/3 overflow-auto flex flex-col justify-between px-1 scrollbar-thin">
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-auto flex flex-col justify-between px-1 scrollbar-thin flex-1",
|
||||
isMobile ? "w-full" : "w-2/3",
|
||||
)}
|
||||
>
|
||||
{activeTab === "note" && (
|
||||
<NoteContent
|
||||
onSubmit={handleNoteSubmit}
|
||||
|
|
@ -288,13 +315,22 @@ export function AddDocument({
|
|||
{activeTab === "connect" && (
|
||||
<ConnectContent selectedProject={localSelectedProject} />
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<SpaceSelector
|
||||
value={localSelectedProject}
|
||||
onValueChange={setLocalSelectedProject}
|
||||
variant="insideOut"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 pt-3",
|
||||
isMobile ? "flex-col" : "justify-between",
|
||||
)}
|
||||
>
|
||||
{!isMobile && (
|
||||
<SpaceSelector
|
||||
value={localSelectedProject}
|
||||
onValueChange={setLocalSelectedProject}
|
||||
variant="insideOut"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn("flex items-center gap-2", isMobile && "justify-end")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
|
|
@ -317,14 +353,16 @@ export function AddDocument({
|
|||
) : (
|
||||
<>
|
||||
+ Add {activeTab}{" "}
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
⌘+Enter
|
||||
</span>
|
||||
{!isMobile && (
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
⌘+Enter
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -343,6 +381,7 @@ function TabButton({
|
|||
title,
|
||||
description,
|
||||
isPro,
|
||||
compact,
|
||||
}: {
|
||||
active: boolean
|
||||
onClick: () => void
|
||||
|
|
@ -350,7 +389,34 @@ function TabButton({
|
|||
title: string
|
||||
description: string
|
||||
isPro?: boolean
|
||||
compact?: boolean
|
||||
}) {
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-full text-left transition-colors whitespace-nowrap focus:outline-none focus:ring-0 shrink-0",
|
||||
active ? "bg-[#14161A] shadow-inside-out" : "hover:bg-[#14161A]/50",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("size-4 shrink-0 text-white")} />
|
||||
<span
|
||||
className={cn("font-medium text-white text-sm", dmSansClassName())}
|
||||
>
|
||||
{title.split(" ")[0]}
|
||||
</span>
|
||||
{isPro && (
|
||||
<span className="bg-[#4BA0FA] text-black text-[8px] font-semibold px-1 py-0.5 rounded">
|
||||
PRO
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -8,19 +8,57 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|||
import { XIcon, Loader2 } from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { useProjectMutations } from "@/hooks/use-project-mutations"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@ui/components/popover"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
|
||||
|
||||
const EMOJI_LIST = [
|
||||
"📁", "📂", "🗂️", "📚", "📖", "📝", "✏️", "📌",
|
||||
"🎯", "🚀", "💡", "⭐", "🔥", "💎", "🎨", "🎵",
|
||||
"🏠", "💼", "🛠️", "⚙️", "🔧", "📊", "📈", "💰",
|
||||
"🌟", "✨", "🌈", "🌸", "🌺", "🍀", "🌿", "🌴",
|
||||
"🐶", "🐱", "🦊", "🦁", "🐼", "🐨", "🦄", "🐝",
|
||||
"❤️", "💜", "💙", "💚", "💛", "🧡", "🖤", "🤍",
|
||||
"📁",
|
||||
"📂",
|
||||
"🗂️",
|
||||
"📚",
|
||||
"📖",
|
||||
"📝",
|
||||
"✏️",
|
||||
"📌",
|
||||
"🎯",
|
||||
"🚀",
|
||||
"💡",
|
||||
"⭐",
|
||||
"🔥",
|
||||
"💎",
|
||||
"🎨",
|
||||
"🎵",
|
||||
"🏠",
|
||||
"💼",
|
||||
"🛠️",
|
||||
"⚙️",
|
||||
"🔧",
|
||||
"📊",
|
||||
"📈",
|
||||
"💰",
|
||||
"🌟",
|
||||
"✨",
|
||||
"🌈",
|
||||
"🌸",
|
||||
"🌺",
|
||||
"🍀",
|
||||
"🌿",
|
||||
"🌴",
|
||||
"🐶",
|
||||
"🐱",
|
||||
"🦊",
|
||||
"🦁",
|
||||
"🐼",
|
||||
"🐨",
|
||||
"🦄",
|
||||
"🐝",
|
||||
"❤️",
|
||||
"💜",
|
||||
"💙",
|
||||
"💚",
|
||||
"💛",
|
||||
"🧡",
|
||||
"🖤",
|
||||
"🤍",
|
||||
]
|
||||
|
||||
export function AddSpaceModal({
|
||||
|
|
@ -56,7 +94,11 @@ export function AddSpaceModal({
|
|||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && spaceName.trim() && !createProjectMutation.isPending) {
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
spaceName.trim() &&
|
||||
!createProjectMutation.isPending
|
||||
) {
|
||||
e.preventDefault()
|
||||
handleCreate()
|
||||
}
|
||||
|
|
@ -83,11 +125,21 @@ export function AddSpaceModal({
|
|||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="pl-1 space-y-1 flex-1">
|
||||
<p className={cn("font-semibold text-[#fafafa]", dmSans125ClassName())}>
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-[#fafafa]",
|
||||
dmSans125ClassName(),
|
||||
)}
|
||||
>
|
||||
Create new space
|
||||
</p>
|
||||
<p className={cn("text-[#737373] font-medium text-[16px] leading-[1.35]")}>
|
||||
Create spaces to organize your memories and documents and create a context rich environment
|
||||
<p
|
||||
className={cn(
|
||||
"text-[#737373] font-medium text-[16px] leading-[1.35]",
|
||||
)}
|
||||
>
|
||||
Create spaces to organize your memories and documents and create
|
||||
a context rich environment
|
||||
</p>
|
||||
</div>
|
||||
<DialogPrimitive.Close
|
||||
|
|
@ -133,7 +185,8 @@ export function AddSpaceModal({
|
|||
onClick={() => handleEmojiSelect(e)}
|
||||
className={cn(
|
||||
"size-8 flex items-center justify-center rounded-md text-lg cursor-pointer transition-colors hover:bg-[#1B1F24]",
|
||||
emoji === e && "bg-[#1B1F24] ring-1 ring-[rgba(115,115,115,0.3)]",
|
||||
emoji === e &&
|
||||
"bg-[#1B1F24] ring-1 ring-[rgba(115,115,115,0.3)]",
|
||||
)}
|
||||
>
|
||||
{e}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
PanelRightCloseIcon,
|
||||
SearchIcon,
|
||||
SquarePenIcon,
|
||||
XIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { dmSansClassName } from "@/lib/fonts"
|
||||
|
|
@ -24,6 +25,8 @@ import { SuperLoader } from "../../superloader"
|
|||
import { UserMessage } from "./message/user-message"
|
||||
import { AgentMessage } from "./message/agent-message"
|
||||
import { ChainOfThought } from "./input/chain-of-thought"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
function ChatEmptyStatePlaceholder({
|
||||
onSuggestionClick,
|
||||
|
|
@ -78,6 +81,7 @@ export function ChatSidebar({
|
|||
isChatOpen: boolean
|
||||
setIsChatOpen: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [input, setInput] = useState("")
|
||||
const [selectedModel, setSelectedModel] = useState<ModelId>("gemini-2.5-pro")
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null)
|
||||
|
|
@ -100,8 +104,10 @@ export function ChatSidebar({
|
|||
const { selectedProject } = useProject()
|
||||
const { setCurrentChatId } = usePersistentChat()
|
||||
|
||||
// Adjust chat height based on scroll position
|
||||
// Adjust chat height based on scroll position (desktop only)
|
||||
useEffect(() => {
|
||||
if (isMobile) return
|
||||
|
||||
const handleWindowScroll = () => {
|
||||
const scrollThreshold = 80
|
||||
const scrollY = window.scrollY
|
||||
|
|
@ -114,7 +120,7 @@ export function ChatSidebar({
|
|||
handleWindowScroll()
|
||||
|
||||
return () => window.removeEventListener("scroll", handleWindowScroll)
|
||||
}, [])
|
||||
}, [isMobile])
|
||||
|
||||
const { messages, sendMessage, status, setMessages, stop } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
|
|
@ -293,6 +299,7 @@ export function ChatSidebar({
|
|||
}, [])
|
||||
|
||||
const handleNewChat = useCallback(() => {
|
||||
analytics.newChatCreated()
|
||||
const newId = crypto.randomUUID()
|
||||
setCurrentChatId(newId)
|
||||
setMessages([])
|
||||
|
|
@ -374,39 +381,59 @@ export function ChatSidebar({
|
|||
<motion.div
|
||||
key="closed"
|
||||
className={cn(
|
||||
"absolute top-0 right-0 flex items-start justify-start m-4",
|
||||
"flex items-start justify-start",
|
||||
isMobile
|
||||
? "fixed bottom-4 right-4 z-50"
|
||||
: "absolute top-0 right-0 m-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
layoutId="chat-toggle-button"
|
||||
>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border border-[#17181A] text-white cursor-pointer whitespace-nowrap"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border border-[#17181A] text-white cursor-pointer whitespace-nowrap shadow-lg",
|
||||
isMobile && "px-4 py-2",
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<NovaOrb size={24} className="blur-[0.6px]! z-10" />
|
||||
Chat with Nova
|
||||
{!isMobile && "Chat with Nova"}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="open"
|
||||
className={cn(
|
||||
"w-[450px] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative pt-4",
|
||||
"bg-[#05070A] backdrop-blur-md flex flex-col border border-[#17181AB2] relative pt-4",
|
||||
isMobile
|
||||
? "fixed inset-0 z-50 w-full h-dvh rounded-none m-0"
|
||||
: "w-[450px] rounded-2xl m-4 mt-2",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
height: `calc(100vh - ${heightOffset}px)`,
|
||||
}}
|
||||
initial={{ x: "100px", opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: "100px", opacity: 0 }}
|
||||
style={
|
||||
isMobile
|
||||
? undefined
|
||||
: {
|
||||
height: `calc(100vh - ${heightOffset}px)`,
|
||||
}
|
||||
}
|
||||
initial={
|
||||
isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
|
||||
}
|
||||
animate={{ x: 0, y: 0, opacity: 1 }}
|
||||
exit={
|
||||
isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 flex items-center justify-between pt-4 px-4 rounded-t-2xl"
|
||||
className={cn(
|
||||
"absolute top-0 left-0 right-0 flex items-center justify-between pt-4 px-4",
|
||||
!isMobile && "rounded-t-2xl",
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, #0A0E14 40.49%, rgba(10, 14, 20, 0.00) 100%)",
|
||||
|
|
@ -417,15 +444,17 @@ export function ChatSidebar({
|
|||
onModelChange={setSelectedModel}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10! border-[#73737333] bg-[#0D121A]"
|
||||
style={{
|
||||
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
|
||||
}}
|
||||
>
|
||||
<HistoryIcon className="size-4 text-[#737373]" />
|
||||
</Button>
|
||||
{!isMobile && (
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10! border-[#73737333] bg-[#0D121A]"
|
||||
style={{
|
||||
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
|
||||
}}
|
||||
>
|
||||
<HistoryIcon className="size-4 text-[#737373]" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-3 h-10! border-[#73737333] bg-[#0D121A] cursor-pointer"
|
||||
|
|
@ -436,21 +465,38 @@ export function ChatSidebar({
|
|||
title="New chat (T)"
|
||||
>
|
||||
<SquarePenIcon className="size-4 text-[#737373]" />
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
T
|
||||
</span>
|
||||
{!isMobile && (
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
T
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer"
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer",
|
||||
isMobile && "bg-[#0D121A] border border-[#73737333]",
|
||||
)}
|
||||
style={
|
||||
isMobile
|
||||
? {
|
||||
boxShadow:
|
||||
"1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
layoutId="chat-toggle-button"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
{isMobile ? (
|
||||
<XIcon className="size-4" />
|
||||
) : (
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,13 @@ function extractTextFromNode(node: TipTapNode): string {
|
|||
}
|
||||
}
|
||||
|
||||
const blockTypes = ["paragraph", "heading", "listItem", "blockquote", "codeBlock"]
|
||||
const blockTypes = [
|
||||
"paragraph",
|
||||
"heading",
|
||||
"listItem",
|
||||
"blockquote",
|
||||
"codeBlock",
|
||||
]
|
||||
if (blockTypes.includes(node.type)) {
|
||||
return `${texts.join("")}\n`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,27 @@
|
|||
import React from "react"
|
||||
import { Streamdown } from "streamdown"
|
||||
|
||||
const components = {
|
||||
p: ({ children, ...props }: React.ComponentPropsWithoutRef<"p">) => {
|
||||
const hasDiv = React.Children.toArray(children).some(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type === "string" &&
|
||||
child.type === "div",
|
||||
)
|
||||
|
||||
if (hasDiv) {
|
||||
return <div {...props}>{children}</div>
|
||||
}
|
||||
|
||||
return <p {...props}>{children}</p>
|
||||
},
|
||||
} as const
|
||||
|
||||
export function NotionDoc({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
|
||||
<Streamdown>{content}</Streamdown>
|
||||
<Streamdown components={components}>{content}</Streamdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
27
apps/web/components/new/document-modal/content/web-page.tsx
Normal file
27
apps/web/components/new/document-modal/content/web-page.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from "react"
|
||||
import { Streamdown } from "streamdown"
|
||||
|
||||
const components = {
|
||||
p: ({ children, ...props }: React.ComponentPropsWithoutRef<"p">) => {
|
||||
const hasDiv = React.Children.toArray(children).some(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type === "string" &&
|
||||
child.type === "div",
|
||||
)
|
||||
|
||||
if (hasDiv) {
|
||||
return <div {...props}>{children}</div>
|
||||
}
|
||||
|
||||
return <p {...props}>{children}</p>
|
||||
},
|
||||
} as const
|
||||
|
||||
export function WebPageContent({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
|
||||
<Streamdown components={components}>{content}</Streamdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,13 @@
|
|||
|
||||
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import { ArrowUpRightIcon, XIcon, Loader2, Trash2Icon, CheckIcon } from "lucide-react"
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
XIcon,
|
||||
Loader2,
|
||||
Trash2Icon,
|
||||
CheckIcon,
|
||||
} from "lucide-react"
|
||||
import type { z } from "zod"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { cn } from "@lib/utils"
|
||||
|
|
@ -22,6 +28,8 @@ import { Button } from "@repo/ui/components/button"
|
|||
import { useDocumentMutations } from "@/hooks/use-document-mutations"
|
||||
import type { UseMutationResult } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { WebPageContent } from "./content/web-page"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
|
||||
// Dynamically importing to prevent DOMMatrix error
|
||||
const PdfViewer = dynamic(
|
||||
|
|
@ -61,7 +69,11 @@ function isTemporaryId(id: string | null | undefined): boolean {
|
|||
return id.startsWith("temp-") || id.startsWith("temp-file-")
|
||||
}
|
||||
|
||||
function DeleteButton({ documentId, customId, deleteMutation }: DeleteButtonProps) {
|
||||
function DeleteButton({
|
||||
documentId,
|
||||
customId,
|
||||
deleteMutation,
|
||||
}: DeleteButtonProps) {
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
|
|
@ -140,6 +152,7 @@ export function DocumentModal({
|
|||
isOpen,
|
||||
onClose,
|
||||
}: DocumentModalProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const { updateMutation, deleteMutation } = useDocumentMutations({ onClose })
|
||||
|
||||
const { initialEditorContent, initialEditorString } = useMemo(() => {
|
||||
|
|
@ -181,7 +194,9 @@ export function DocumentModal({
|
|||
if (!_document?.id) return
|
||||
updateMutation.mutate(
|
||||
{ documentId: _document.id, content: draftContentString },
|
||||
{ onSuccess: (_data, variables) => setLastSavedContent(variables.content) },
|
||||
{
|
||||
onSuccess: (_data, variables) => setLastSavedContent(variables.content),
|
||||
},
|
||||
)
|
||||
}, [_document?.id, draftContentString, updateMutation])
|
||||
|
||||
|
|
@ -189,7 +204,10 @@ export function DocumentModal({
|
|||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! p-0 border-none bg-[#1B1F24] flex flex-col px-4 pt-3 pb-4 gap-3 rounded-[22px]",
|
||||
"p-0 border-none bg-[#1B1F24] flex flex-col px-3 md:px-4 pt-3 pb-4 gap-3",
|
||||
isMobile
|
||||
? "w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-xl"
|
||||
: "w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! rounded-[22px]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -201,7 +219,7 @@ export function DocumentModal({
|
|||
<DialogTitle className="sr-only">
|
||||
{_document?.title} - Document
|
||||
</DialogTitle>
|
||||
<div className="flex items-center justify-between h-fit gap-4">
|
||||
<div className="flex items-center justify-between h-fit gap-2 md:gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<Title
|
||||
title={_document?.title}
|
||||
|
|
@ -209,7 +227,7 @@ export function DocumentModal({
|
|||
url={_document?.url}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
|
||||
<DeleteButton
|
||||
documentId={_document?.id}
|
||||
customId={_document?.customId}
|
||||
|
|
@ -220,9 +238,14 @@ export function DocumentModal({
|
|||
href={_document.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="line-clamp-1 px-3 py-2 flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
|
||||
className={cn(
|
||||
"flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]",
|
||||
isMobile ? "w-7 h-7 justify-center" : "px-3 py-2",
|
||||
)}
|
||||
>
|
||||
Visit source
|
||||
{!isMobile && (
|
||||
<span className="line-clamp-1">Visit source</span>
|
||||
)}
|
||||
<ArrowUpRightIcon className="w-4 h-4 text-[#737373]" />
|
||||
</a>
|
||||
)}
|
||||
|
|
@ -237,7 +260,7 @@ export function DocumentModal({
|
|||
</DialogPrimitive.Close>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 grid grid-cols-[2fr_1fr] gap-3 overflow-hidden min-h-0">
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-3 overflow-hidden min-h-0">
|
||||
<div
|
||||
id="document-preview"
|
||||
className={cn(
|
||||
|
|
@ -322,6 +345,9 @@ export function DocumentModal({
|
|||
{_document?.url?.includes("youtube.com") && (
|
||||
<YoutubeVideo url={_document.url} />
|
||||
)}
|
||||
{_document?.type === "webpage" && (
|
||||
<WebPageContent content={_document.content ?? ""} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
id="document-memories-summary"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import {
|
|||
Home,
|
||||
Code2,
|
||||
ExternalLink,
|
||||
HelpCircle,
|
||||
MenuIcon,
|
||||
MessageCircleIcon,
|
||||
} from "lucide-react"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { cn } from "@lib/utils"
|
||||
|
|
@ -21,6 +24,7 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu"
|
||||
import { authClient } from "@lib/auth"
|
||||
|
|
@ -29,17 +33,20 @@ import { useProject } from "@/stores"
|
|||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { SpaceSelector } from "./space-selector"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
|
||||
interface HeaderProps {
|
||||
onAddMemory?: () => void
|
||||
onOpenMCP?: () => void
|
||||
onOpenChat?: () => void
|
||||
}
|
||||
|
||||
export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
||||
export function Header({ onAddMemory, onOpenMCP, onOpenChat }: HeaderProps) {
|
||||
const { user } = useAuth()
|
||||
const { selectedProject } = useProject()
|
||||
const { switchProject } = useProjectMutations()
|
||||
const router = useRouter()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const displayName =
|
||||
user?.displayUsername ||
|
||||
|
|
@ -48,16 +55,16 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
|||
""
|
||||
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
||||
return (
|
||||
<div className="flex p-4 justify-between items-center">
|
||||
<div className="flex items-center justify-center gap-4 z-10!">
|
||||
<div className="flex p-3 md:p-4 justify-between items-center gap-2">
|
||||
<div className="flex items-center justify-center gap-2 md:gap-4 z-10! min-w-0">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors"
|
||||
className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors shrink-0"
|
||||
>
|
||||
<Logo className="h-7" />
|
||||
{userName && (
|
||||
{!isMobile && userName && (
|
||||
<div className="flex flex-col items-start justify-center ml-2">
|
||||
<p className="text-[#8B8B8B] text-[11px] leading-tight">
|
||||
{userName}
|
||||
|
|
@ -71,143 +78,276 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-56 bg-[#0D121A] rounded-xl border-none p-1.5 ml-4 shadow-[0_0_20px_rgba(255,255,255,0.15)]"
|
||||
className={cn(
|
||||
"min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<Link href="/new">
|
||||
<Home className="h-4 w-4" />
|
||||
<Home className="h-4 w-4 text-[#737373]" />
|
||||
Home
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<a
|
||||
href="https://console.supermemory.ai"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Code2 className="h-4 w-4" />
|
||||
<Code2 className="h-4 w-4 text-[#737373]" />
|
||||
Developer console
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<a href="https://supermemory.ai" target="_blank" rel="noreferrer">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<ExternalLink className="h-4 w-4 text-[#737373]" />
|
||||
supermemory.ai
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="self-stretch w-px bg-[#FFFFFF33]" />
|
||||
<SpaceSelector
|
||||
value={selectedProject}
|
||||
onValueChange={switchProject}
|
||||
showChevron
|
||||
enableDelete
|
||||
/>
|
||||
<div className="self-stretch w-px bg-[#FFFFFF33] hidden md:block" />
|
||||
{!isMobile && (
|
||||
<SpaceSelector
|
||||
value={selectedProject}
|
||||
onValueChange={switchProject}
|
||||
showChevron
|
||||
enableDelete
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Tabs defaultValue="grid">
|
||||
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
|
||||
<TabsTrigger
|
||||
value="grid"
|
||||
className={cn(
|
||||
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
Grid
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="graph"
|
||||
className={cn(
|
||||
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
Graph
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex items-center gap-2 z-10!">
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
onClick={onAddMemory}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="size-4" />
|
||||
Add memory
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
C
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
onClick={onOpenMCP}
|
||||
>
|
||||
<div className="flex items-center gap-2">MCP</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
>
|
||||
<SearchIcon className="size-4" />
|
||||
<span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
|
||||
<svg
|
||||
className="size-[7.5px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{!isMobile && (
|
||||
<Tabs defaultValue="grid">
|
||||
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
|
||||
<TabsTrigger
|
||||
value="grid"
|
||||
className={cn(
|
||||
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<title>Search Icon</title>
|
||||
<path
|
||||
d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className={cn(dmSansClassName())}>K</span>
|
||||
</span>
|
||||
</Button>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
Grid
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="graph"
|
||||
className={cn(
|
||||
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<LayoutGridIcon className="size-4" />
|
||||
Graph
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
<div className="flex items-center gap-2 z-10!">
|
||||
{isMobile ? (
|
||||
<>
|
||||
<SpaceSelector
|
||||
value={selectedProject}
|
||||
onValueChange={switchProject}
|
||||
showChevron
|
||||
enableDelete
|
||||
compact
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
>
|
||||
<MenuIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className={cn(
|
||||
"min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={onAddMemory}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4 text-[#737373]" />
|
||||
Add memory
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onOpenMCP}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<Code2 className="h-4 w-4 text-[#737373]" />
|
||||
MCP
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onOpenChat}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<MessageCircleIcon className="h-4 w-4 text-[#737373]" />
|
||||
Chat with Nova
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push("/new/settings")}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<Settings className="h-4 w-4 text-[#737373]" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
onClick={onAddMemory}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="size-4" />
|
||||
Add memory
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
C
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
onClick={onOpenMCP}
|
||||
>
|
||||
<div className="flex items-center gap-2">MCP</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-2 h-10!"
|
||||
>
|
||||
<SearchIcon className="size-4" />
|
||||
<span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
|
||||
<svg
|
||||
className="size-[7.5px]"
|
||||
viewBox="0 0 9 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Search Icon</title>
|
||||
<path
|
||||
d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
|
||||
stroke="#737373"
|
||||
strokeWidth="0.833333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className={cn(dmSansClassName())}>K</span>
|
||||
</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10 cursor-pointer">
|
||||
<AvatarImage src={user?.image ?? ""} />
|
||||
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-transform hover:scale-105"
|
||||
>
|
||||
<Avatar className="border border-[#2E3033] h-8 w-8 md:h-10 md:w-10">
|
||||
<AvatarImage src={user?.image ?? ""} />
|
||||
<AvatarFallback className="bg-[#0D121A] text-white">
|
||||
{user?.name?.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => router.push("/new/settings")}>
|
||||
<Settings className="h-4 w-4" />
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className={cn(
|
||||
"min-w-[220px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<div id="user-info" className="px-3 py-2.5">
|
||||
<p className="text-white text-sm font-medium truncate">
|
||||
{user?.name}
|
||||
</p>
|
||||
<p className="text-[#737373] text-xs truncate">{user?.email}</p>
|
||||
</div>
|
||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push("/new/settings")}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<Settings className="h-4 w-4 text-[#737373]" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<a href="mailto:support@supermemory.com">
|
||||
<HelpCircle className="h-4 w-4 text-[#737373]" />
|
||||
Help & Support
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<a
|
||||
href="https://supermemory.link/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4 text-[#737373]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title>Discord</title>
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
authClient.signOut()
|
||||
router.push("/login/new")
|
||||
}}
|
||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<LogOut className="h-4 w-4 text-[#737373]" />
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -67,7 +67,11 @@ export function MCPModal({
|
|||
Migrate from MCP v1
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="insideOut" className="px-6 py-[10px]" onClick={onClose}>
|
||||
<Button
|
||||
variant="insideOut"
|
||||
className="px-6 py-[10px]"
|
||||
onClick={onClose}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { dmSansClassName } from "@/lib/fonts"
|
|||
import { useAuth } from "@lib/auth-context"
|
||||
import { useProject } from "@/stores"
|
||||
import { Streamdown } from "streamdown"
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
|
||||
interface ChatSidebarProps {
|
||||
formData: {
|
||||
|
|
@ -35,8 +36,9 @@ interface DraftDoc {
|
|||
export function ChatSidebar({ formData }: ChatSidebarProps) {
|
||||
const { user } = useAuth()
|
||||
const { selectedProject } = useProject()
|
||||
const isMobile = useIsMobile()
|
||||
const [message, setMessage] = useState("")
|
||||
const [isChatOpen, setIsChatOpen] = useState(true)
|
||||
const [isChatOpen, setIsChatOpen] = useState(!isMobile)
|
||||
const [timelineMessages, setTimelineMessages] = useState<
|
||||
{
|
||||
message: string
|
||||
|
|
@ -405,44 +407,73 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
|
|||
<motion.div
|
||||
key="closed"
|
||||
className={cn(
|
||||
"absolute top-0 right-0 flex items-start justify-start m-4",
|
||||
"flex items-start justify-start",
|
||||
isMobile
|
||||
? "fixed bottom-4 right-4 z-50"
|
||||
: "absolute top-0 right-0 m-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
layoutId="chat-toggle-button"
|
||||
>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer"
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer shadow-lg",
|
||||
isMobile && "px-4 py-2",
|
||||
)}
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<NovaOrb size={24} className="blur-none! z-10" />
|
||||
Chat with Nova
|
||||
{!isMobile && "Chat with Nova"}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="open"
|
||||
className={cn(
|
||||
"w-[450px] h-[calc(100vh-110px)] bg-[#0A0E14] backdrop-blur-md flex flex-col rounded-2xl m-4",
|
||||
"bg-[#0A0E14] backdrop-blur-md flex flex-col",
|
||||
isMobile
|
||||
? "fixed inset-0 z-50 w-full h-dvh rounded-none m-0"
|
||||
: "w-[450px] h-[calc(100vh-110px)] rounded-2xl m-4",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
initial={{ x: "100px", opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: "100px", opacity: 0 }}
|
||||
initial={
|
||||
isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
|
||||
}
|
||||
animate={{ x: 0, y: 0, opacity: 1 }}
|
||||
exit={
|
||||
isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
|
||||
>
|
||||
<motion.button
|
||||
onClick={toggleChat}
|
||||
className="absolute top-4 right-4 flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
className={cn(
|
||||
"absolute top-4 right-4 flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer",
|
||||
isMobile && "bg-[#0D121A] border border-[#73737333]",
|
||||
)}
|
||||
style={
|
||||
isMobile
|
||||
? {
|
||||
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
|
||||
}
|
||||
: {
|
||||
background:
|
||||
"linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}
|
||||
}
|
||||
layoutId="chat-toggle-button"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
Close chat
|
||||
{isMobile ? (
|
||||
<XIcon className="size-4" />
|
||||
) : (
|
||||
<>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
Close chat
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
<div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end overflow-y-auto scrollbar-thin">
|
||||
{timelineMessages.map((msg, i) => (
|
||||
|
|
@ -661,7 +692,8 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
|
|||
xResearchStatus === "correct"
|
||||
? "bg-green-500/20 text-green-400 border border-green-500/40"
|
||||
: "bg-[#1B1F24] text-white/50 hover:text-white/70",
|
||||
(isConfirmed || isLoading) && "opacity-50 cursor-not-allowed",
|
||||
(isConfirmed || isLoading) &&
|
||||
"opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="size-3" />
|
||||
|
|
|
|||
|
|
@ -164,9 +164,7 @@ export function IntegrationsStep() {
|
|||
<Button
|
||||
variant="link"
|
||||
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
|
||||
onClick={() =>
|
||||
router.push("/new/onboarding/setup?step=relatable")
|
||||
}
|
||||
onClick={() => router.push("/new/onboarding/setup?step=relatable")}
|
||||
>
|
||||
← Back
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export interface SpaceSelectorProps {
|
|||
contentClassName?: string
|
||||
showNewSpace?: boolean
|
||||
enableDelete?: boolean
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
const triggerVariants = {
|
||||
|
|
@ -53,6 +54,7 @@ export function SpaceSelector({
|
|||
contentClassName,
|
||||
showNewSpace = true,
|
||||
enableDelete = false,
|
||||
compact = false,
|
||||
}: SpaceSelectorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
|
|
@ -87,7 +89,9 @@ export function SpaceSelector({
|
|||
const selectedProject = useMemo(() => {
|
||||
if (value === DEFAULT_PROJECT_ID) return { name: "My Space", emoji: "📁" }
|
||||
const found = projects.find((p: Project) => p.containerTag === value)
|
||||
return found ? { name: found.name, emoji: found.emoji } : { name: value, emoji: undefined }
|
||||
return found
|
||||
? { name: found.name, emoji: found.emoji }
|
||||
: { name: value, emoji: undefined }
|
||||
}, [projects, value])
|
||||
|
||||
const selectedProjectName = selectedProject.name
|
||||
|
|
@ -125,7 +129,9 @@ export function SpaceSelector({
|
|||
projectId: deleteDialog.project.id,
|
||||
action: deleteDialog.action,
|
||||
targetProjectId:
|
||||
deleteDialog.action === "move" ? deleteDialog.targetProjectId : undefined,
|
||||
deleteDialog.action === "move"
|
||||
? deleteDialog.targetProjectId
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
|
@ -156,14 +162,14 @@ export function SpaceSelector({
|
|||
p.id !== deleteDialog.project?.id &&
|
||||
p.containerTag !== deleteDialog.project?.containerTag,
|
||||
)
|
||||
|
||||
|
||||
const defaultProject = projects.find(
|
||||
(p: Project) => p.containerTag === DEFAULT_PROJECT_ID,
|
||||
)
|
||||
|
||||
|
||||
const isDefaultProjectBeingDeleted =
|
||||
deleteDialog.project?.containerTag === DEFAULT_PROJECT_ID
|
||||
|
||||
|
||||
if (defaultProject && !isDefaultProjectBeingDeleted) {
|
||||
const defaultProjectIncluded = filtered.some(
|
||||
(p: Project) => p.containerTag === DEFAULT_PROJECT_ID,
|
||||
|
|
@ -172,7 +178,7 @@ export function SpaceSelector({
|
|||
return [defaultProject, ...filtered]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return filtered
|
||||
}, [projects, deleteDialog.project])
|
||||
|
||||
|
|
@ -189,10 +195,14 @@ export function SpaceSelector({
|
|||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-bold tracking-[-0.98px]">{selectedProjectEmoji}</span>
|
||||
<span className="text-sm font-medium text-white">
|
||||
{isLoading ? "..." : selectedProjectName}
|
||||
<span className="text-sm font-bold tracking-[-0.98px]">
|
||||
{selectedProjectEmoji}
|
||||
</span>
|
||||
{!compact && (
|
||||
<span className="text-sm font-medium text-white">
|
||||
{isLoading ? "..." : selectedProjectName}
|
||||
</span>
|
||||
)}
|
||||
{showChevron && (
|
||||
<ChevronsLeftRight className="size-4 rotate-90 text-white/70" />
|
||||
)}
|
||||
|
|
@ -201,7 +211,7 @@ export function SpaceSelector({
|
|||
<DropdownMenuContent
|
||||
align="start"
|
||||
className={cn(
|
||||
"min-w-[200px] p-3 rounded-[11px] border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
"min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||
dmSansClassName(),
|
||||
contentClassName,
|
||||
)}
|
||||
|
|
@ -209,16 +219,16 @@ export function SpaceSelector({
|
|||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col">
|
||||
{/* Default Project - no delete allowed */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelect(DEFAULT_PROJECT_ID)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium",
|
||||
"flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium",
|
||||
value === DEFAULT_PROJECT_ID
|
||||
? "bg-[#161E2B] border border-[rgba(115,115,115,0.1)]"
|
||||
: "opacity-50 hover:opacity-100 hover:bg-[#161E2B]/50",
|
||||
? "bg-[#293952]/40"
|
||||
: "opacity-60 hover:opacity-100 hover:bg-[#293952]/40",
|
||||
)}
|
||||
>
|
||||
<span className="font-bold tracking-[-0.98px]">📁</span>
|
||||
|
|
@ -233,13 +243,15 @@ export function SpaceSelector({
|
|||
key={project.id}
|
||||
onClick={() => handleSelect(project.containerTag)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium group",
|
||||
"flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium group",
|
||||
value === project.containerTag
|
||||
? "bg-[#161E2B] border border-[rgba(115,115,115,0.1)]"
|
||||
: "opacity-50 hover:opacity-100 hover:bg-[#161E2B]/50",
|
||||
? "bg-[#293952]/40"
|
||||
: "opacity-60 hover:opacity-100 hover:bg-[#293952]/40",
|
||||
)}
|
||||
>
|
||||
<span className="font-bold tracking-[-0.98px]">{project.emoji || "📁"}</span>
|
||||
<span className="font-bold tracking-[-0.98px]">
|
||||
{project.emoji || "📁"}
|
||||
</span>
|
||||
<span className="truncate flex-1">{project.name}</span>
|
||||
{enableDelete && (
|
||||
<button
|
||||
|
|
@ -309,9 +321,17 @@ export function SpaceSelector({
|
|||
showCloseButton={false}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div id="delete-dialog-header" className="flex justify-between items-start gap-4">
|
||||
<div
|
||||
id="delete-dialog-header"
|
||||
className="flex justify-between items-start gap-4"
|
||||
>
|
||||
<div className="pl-1 space-y-1 flex-1">
|
||||
<p className={cn("font-semibold text-[#fafafa]", dmSans125ClassName())}>
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-[#fafafa]",
|
||||
dmSans125ClassName(),
|
||||
)}
|
||||
>
|
||||
Delete space
|
||||
</p>
|
||||
<p className="text-[#737373] font-medium text-[16px] leading-[1.35]">
|
||||
|
|
@ -325,7 +345,8 @@ export function SpaceSelector({
|
|||
<DialogPrimitive.Close
|
||||
className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 border border-[rgba(115,115,115,0.2)] shrink-0"
|
||||
style={{
|
||||
boxShadow: "inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
|
||||
boxShadow:
|
||||
"inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
|
||||
}}
|
||||
>
|
||||
<XIcon stroke="#737373" />
|
||||
|
|
@ -337,7 +358,9 @@ export function SpaceSelector({
|
|||
<button
|
||||
id="move-option"
|
||||
type="button"
|
||||
onClick={() => setDeleteDialog((prev) => ({ ...prev, action: "move" }))}
|
||||
onClick={() =>
|
||||
setDeleteDialog((prev) => ({ ...prev, action: "move" }))
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
|
||||
deleteDialog.action === "move"
|
||||
|
|
@ -415,7 +438,9 @@ export function SpaceSelector({
|
|||
<span className="flex items-center gap-2">
|
||||
<span>{p.emoji || "📁"}</span>
|
||||
<span>
|
||||
{p.containerTag === DEFAULT_PROJECT_ID ? "My Space" : p.name}
|
||||
{p.containerTag === DEFAULT_PROJECT_ID
|
||||
? "My Space"
|
||||
: p.name}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
|
|
@ -428,7 +453,9 @@ export function SpaceSelector({
|
|||
<button
|
||||
id="delete-option"
|
||||
type="button"
|
||||
onClick={() => setDeleteDialog((prev) => ({ ...prev, action: "delete" }))}
|
||||
onClick={() =>
|
||||
setDeleteDialog((prev) => ({ ...prev, action: "delete" }))
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
|
||||
deleteDialog.action === "delete"
|
||||
|
|
@ -470,7 +497,10 @@ export function SpaceSelector({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div id="delete-dialog-footer" className="flex items-center justify-end gap-[22px]">
|
||||
<div
|
||||
id="delete-dialog-footer"
|
||||
className="flex items-center justify-end gap-[22px]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteCancel}
|
||||
|
|
@ -487,7 +517,8 @@ export function SpaceSelector({
|
|||
onClick={handleDeleteConfirm}
|
||||
disabled={
|
||||
deleteProjectMutation.isPending ||
|
||||
(deleteDialog.action === "move" && !deleteDialog.targetProjectId)
|
||||
(deleteDialog.action === "move" &&
|
||||
!deleteDialog.targetProjectId)
|
||||
}
|
||||
className={cn(
|
||||
"px-4 py-[10px] rounded-full",
|
||||
|
|
@ -498,7 +529,9 @@ export function SpaceSelector({
|
|||
{deleteProjectMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin mr-2" />
|
||||
{deleteDialog.action === "move" ? "Moving..." : "Deleting..."}
|
||||
{deleteDialog.action === "move"
|
||||
? "Moving..."
|
||||
: "Deleting..."}
|
||||
</>
|
||||
) : deleteDialog.action === "move" ? (
|
||||
"Move & Delete"
|
||||
|
|
|
|||
|
|
@ -82,28 +82,25 @@ export function TextEditor({
|
|||
}
|
||||
}, [editor, initialContent])
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest(".ProseMirror")) {
|
||||
return
|
||||
}
|
||||
if (target.closest("button, a")) {
|
||||
return
|
||||
}
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest(".ProseMirror")) {
|
||||
return
|
||||
}
|
||||
if (target.closest("button, a")) {
|
||||
return
|
||||
}
|
||||
|
||||
const proseMirror = containerRef.current?.querySelector(
|
||||
".ProseMirror",
|
||||
) as HTMLElement
|
||||
if (proseMirror && editorRef.current) {
|
||||
setTimeout(() => {
|
||||
proseMirror.focus()
|
||||
editorRef.current?.commands.focus("end")
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
const proseMirror = containerRef.current?.querySelector(
|
||||
".ProseMirror",
|
||||
) as HTMLElement
|
||||
if (proseMirror && editorRef.current) {
|
||||
setTimeout(() => {
|
||||
proseMirror.focus()
|
||||
editorRef.current?.commands.focus("end")
|
||||
}, 0)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -132,9 +129,7 @@ export function TextEditor({
|
|||
<div className="flex items-center gap-1 rounded-[8px] bg-[#1b1f24] p-2 shadow-[0px_4px_20px_0px_rgba(0,0,0,0.25),inset_1px_1px_1px_0px_rgba(255,255,255,0.1)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleBold().run()
|
||||
}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
|
||||
editor.isActive("bold") && "bg-[#2e353d]",
|
||||
|
|
@ -144,9 +139,7 @@ export function TextEditor({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleItalic().run()
|
||||
}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
|
||||
editor.isActive("italic") && "bg-[#2e353d]",
|
||||
|
|
@ -156,9 +149,7 @@ export function TextEditor({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleCode().run()
|
||||
}
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
|
||||
editor.isActive("code") && "bg-[#2e353d]",
|
||||
|
|
|
|||
|
|
@ -169,9 +169,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
|
|||
onStart: (props) => {
|
||||
selectedIndex = 0
|
||||
currentItems = props.items as SuggestionItem[]
|
||||
currentCommand = props.command as (
|
||||
item: SuggestionItem,
|
||||
) => void
|
||||
currentCommand = props.command as (item: SuggestionItem) => void
|
||||
currentClientRect = props.clientRect ?? null
|
||||
|
||||
const element = document.createElement("div")
|
||||
|
|
@ -208,9 +206,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
|
|||
|
||||
onUpdate: (props) => {
|
||||
currentItems = props.items as SuggestionItem[]
|
||||
currentCommand = props.command as (
|
||||
item: SuggestionItem,
|
||||
) => void
|
||||
currentCommand = props.command as (item: SuggestionItem) => void
|
||||
currentClientRect = props.clientRect ?? null
|
||||
|
||||
if (selectedIndex >= currentItems.length) {
|
||||
|
|
@ -238,9 +234,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
|
|||
|
||||
if (event.key === "ArrowUp") {
|
||||
selectedIndex =
|
||||
selectedIndex <= 0
|
||||
? currentItems.length - 1
|
||||
: selectedIndex - 1
|
||||
selectedIndex <= 0 ? currentItems.length - 1 : selectedIndex - 1
|
||||
if (currentCommand) {
|
||||
component?.updateProps({
|
||||
items: currentItems,
|
||||
|
|
@ -254,9 +248,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
|
|||
|
||||
if (event.key === "ArrowDown") {
|
||||
selectedIndex =
|
||||
selectedIndex >= currentItems.length - 1
|
||||
? 0
|
||||
: selectedIndex + 1
|
||||
selectedIndex >= currentItems.length - 1 ? 0 : selectedIndex + 1
|
||||
if (currentCommand) {
|
||||
component?.updateProps({
|
||||
items: currentItems,
|
||||
|
|
|
|||
|
|
@ -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,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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ export function ConnectionsTabContent() {
|
|||
// Add connection mutation
|
||||
const addConnectionMutation = useMutation({
|
||||
mutationFn: async (provider: ConnectorProvider) => {
|
||||
|
||||
// Check if user can add connections
|
||||
if (!canAddConnection && !isProUser) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { toast } from "sonner"
|
|||
import { $fetch } from "@lib/api"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import type { z } from "zod"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
|
||||
|
|
@ -29,7 +30,9 @@ interface UseDocumentMutationsOptions {
|
|||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions = {}) {
|
||||
export function useDocumentMutations({
|
||||
onClose,
|
||||
}: UseDocumentMutationsOptions = {}) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const noteMutation = useMutation({
|
||||
|
|
@ -111,6 +114,10 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
|
|||
})
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
analytics.documentAdded({
|
||||
type: "note",
|
||||
project_id: variables.project,
|
||||
})
|
||||
toast.success("Note added successfully!", {
|
||||
description: "Your note is being processed",
|
||||
})
|
||||
|
|
@ -194,6 +201,10 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
|
|||
})
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
analytics.documentAdded({
|
||||
type: "link",
|
||||
project_id: variables.project,
|
||||
})
|
||||
toast.success("Link added successfully!", {
|
||||
description: "Your link is being processed",
|
||||
})
|
||||
|
|
@ -311,6 +322,10 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
|
|||
})
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
analytics.documentAdded({
|
||||
type: "file",
|
||||
project_id: variables.project,
|
||||
})
|
||||
toast.success("File uploaded successfully!", {
|
||||
description: "Your file is being processed",
|
||||
})
|
||||
|
|
@ -392,7 +407,8 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
|
|||
return {
|
||||
...page,
|
||||
documents: page.documents.filter(
|
||||
(doc) => doc.id !== documentId && doc.customId !== documentId,
|
||||
(doc) =>
|
||||
doc.id !== documentId && doc.customId !== documentId,
|
||||
),
|
||||
pagination: page.pagination
|
||||
? {
|
||||
|
|
@ -412,11 +428,9 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
|
|||
const queryData = old as DocumentsQueryData
|
||||
return {
|
||||
...queryData,
|
||||
documents: queryData.documents.filter(
|
||||
(doc: DocumentWithId) => {
|
||||
return doc.id !== documentId && doc.customId !== documentId
|
||||
},
|
||||
),
|
||||
documents: queryData.documents.filter((doc: DocumentWithId) => {
|
||||
return doc.id !== documentId && doc.customId !== documentId
|
||||
}),
|
||||
totalCount: Math.max(0, (queryData.totalCount ?? 0) - 1),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import posthog from "posthog-js"
|
||||
|
||||
// Helper function to safely capture events
|
||||
const safeCapture = (eventName: string, properties?: Record<string, any>) => {
|
||||
if (posthog.__loaded) {
|
||||
posthog.capture(eventName, properties)
|
||||
}
|
||||
const safeCapture = (eventName: string, properties?: Record<string, unknown>) => {
|
||||
if (posthog.__loaded) {
|
||||
posthog.capture(eventName, properties)
|
||||
}
|
||||
}
|
||||
|
||||
export const analytics = {
|
||||
|
|
@ -47,4 +47,16 @@ export const analytics = {
|
|||
mcpInstallCmdCopied: () => safeCapture("mcp_install_cmd_copied"),
|
||||
|
||||
extensionInstallClicked: () => safeCapture("extension_install_clicked"),
|
||||
|
||||
// nova analytics
|
||||
documentAdded: (props: {
|
||||
type: "note" | "link" | "file" | "connect"
|
||||
project_id?: string
|
||||
}) => safeCapture("document_added", props),
|
||||
|
||||
newChatCreated: () => safeCapture("new_chat_created"),
|
||||
|
||||
mcpModalOpened: () => safeCapture("mcp_modal_opened"),
|
||||
|
||||
addDocumentModalOpened: () => safeCapture("add_document_modal_opened"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,114 +8,123 @@ const filter = args[0] // e.g., "typescript", "python", "integrations", or speci
|
|||
const TESTS_DIR = path.join(import.meta.dir, "tests")
|
||||
|
||||
interface TestFile {
|
||||
name: string
|
||||
path: string
|
||||
type: "ts" | "py"
|
||||
name: string
|
||||
path: string
|
||||
type: "ts" | "py"
|
||||
}
|
||||
|
||||
function getTests(): TestFile[] {
|
||||
const tests: TestFile[] = []
|
||||
const tests: TestFile[] = []
|
||||
|
||||
// TypeScript tests
|
||||
const tsTests = ["quickstart", "sdk", "search", "user-profiles"]
|
||||
for (const t of tsTests) {
|
||||
tests.push({
|
||||
name: `typescript/${t}`,
|
||||
path: path.join(TESTS_DIR, "typescript", `${t}.ts`),
|
||||
type: "ts",
|
||||
})
|
||||
}
|
||||
// TypeScript tests
|
||||
const tsTests = ["quickstart", "sdk", "search", "user-profiles"]
|
||||
for (const t of tsTests) {
|
||||
tests.push({
|
||||
name: `typescript/${t}`,
|
||||
path: path.join(TESTS_DIR, "typescript", `${t}.ts`),
|
||||
type: "ts",
|
||||
})
|
||||
}
|
||||
|
||||
// Python tests
|
||||
const pyTests = ["quickstart", "sdk", "search", "user_profiles"]
|
||||
for (const t of pyTests) {
|
||||
tests.push({
|
||||
name: `python/${t}`,
|
||||
path: path.join(TESTS_DIR, "python", `${t}.py`),
|
||||
type: "py",
|
||||
})
|
||||
}
|
||||
// Python tests
|
||||
const pyTests = ["quickstart", "sdk", "search", "user_profiles"]
|
||||
for (const t of pyTests) {
|
||||
tests.push({
|
||||
name: `python/${t}`,
|
||||
path: path.join(TESTS_DIR, "python", `${t}.py`),
|
||||
type: "py",
|
||||
})
|
||||
}
|
||||
|
||||
// Integration tests
|
||||
const intTests = [
|
||||
{ name: "ai-sdk", type: "ts" as const },
|
||||
{ name: "openai-sdk", type: "ts" as const },
|
||||
{ name: "openai-sdk", type: "py" as const },
|
||||
{ name: "claude-memory", type: "ts" as const },
|
||||
]
|
||||
for (const t of intTests) {
|
||||
tests.push({
|
||||
name: `integrations/${t.name}`,
|
||||
path: path.join(TESTS_DIR, "integrations", `${t.name}.${t.type === "ts" ? "ts" : "py"}`),
|
||||
type: t.type,
|
||||
})
|
||||
}
|
||||
// Integration tests
|
||||
const intTests = [
|
||||
{ name: "ai-sdk", type: "ts" as const },
|
||||
{ name: "openai-sdk", type: "ts" as const },
|
||||
{ name: "openai-sdk", type: "py" as const },
|
||||
{ name: "claude-memory", type: "ts" as const },
|
||||
]
|
||||
for (const t of intTests) {
|
||||
tests.push({
|
||||
name: `integrations/${t.name}`,
|
||||
path: path.join(
|
||||
TESTS_DIR,
|
||||
"integrations",
|
||||
`${t.name}.${t.type === "ts" ? "ts" : "py"}`,
|
||||
),
|
||||
type: t.type,
|
||||
})
|
||||
}
|
||||
|
||||
return tests
|
||||
return tests
|
||||
}
|
||||
|
||||
async function runTest(test: TestFile): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n${"=".repeat(60)}`)
|
||||
console.log(`Running: ${test.name}`)
|
||||
console.log("=".repeat(60))
|
||||
return new Promise((resolve) => {
|
||||
console.log(`\n${"=".repeat(60)}`)
|
||||
console.log(`Running: ${test.name}`)
|
||||
console.log("=".repeat(60))
|
||||
|
||||
const cmd = test.type === "ts" ? "bun" : path.join(import.meta.dir, ".venv", "bin", "python3")
|
||||
const proc = spawn(cmd, [test.path], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env },
|
||||
})
|
||||
const cmd =
|
||||
test.type === "ts"
|
||||
? "bun"
|
||||
: path.join(import.meta.dir, ".venv", "bin", "python3")
|
||||
const proc = spawn(cmd, [test.path], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env },
|
||||
})
|
||||
|
||||
proc.on("close", (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
})
|
||||
proc.on("close", (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Supermemory Docs Test Runner")
|
||||
console.log("============================\n")
|
||||
console.log("Supermemory Docs Test Runner")
|
||||
console.log("============================\n")
|
||||
|
||||
let tests = getTests()
|
||||
let tests = getTests()
|
||||
|
||||
// Filter tests if specified
|
||||
if (filter) {
|
||||
tests = tests.filter((t) => t.name.includes(filter) || t.type === filter.replace(".", ""))
|
||||
}
|
||||
// Filter tests if specified
|
||||
if (filter) {
|
||||
tests = tests.filter(
|
||||
(t) => t.name.includes(filter) || t.type === filter.replace(".", ""),
|
||||
)
|
||||
}
|
||||
|
||||
if (tests.length === 0) {
|
||||
console.log("No tests matched the filter:", filter)
|
||||
console.log("\nAvailable tests:")
|
||||
getTests().forEach((t) => console.log(` - ${t.name} (${t.type})`))
|
||||
process.exit(1)
|
||||
}
|
||||
if (tests.length === 0) {
|
||||
console.log("No tests matched the filter:", filter)
|
||||
console.log("\nAvailable tests:")
|
||||
getTests().forEach((t) => console.log(` - ${t.name} (${t.type})`))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Running ${tests.length} test(s)...\n`)
|
||||
console.log(`Running ${tests.length} test(s)...\n`)
|
||||
|
||||
const results: { test: string; passed: boolean }[] = []
|
||||
const results: { test: string; passed: boolean }[] = []
|
||||
|
||||
for (const test of tests) {
|
||||
const passed = await runTest(test)
|
||||
results.push({ test: test.name, passed })
|
||||
}
|
||||
for (const test of tests) {
|
||||
const passed = await runTest(test)
|
||||
results.push({ test: test.name, passed })
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log(`\n${"=".repeat(60)}`)
|
||||
console.log("SUMMARY")
|
||||
console.log("=".repeat(60))
|
||||
// Summary
|
||||
console.log(`\n${"=".repeat(60)}`)
|
||||
console.log("SUMMARY")
|
||||
console.log("=".repeat(60))
|
||||
|
||||
const passed = results.filter((r) => r.passed).length
|
||||
const failed = results.filter((r) => !r.passed).length
|
||||
const passed = results.filter((r) => r.passed).length
|
||||
const failed = results.filter((r) => !r.passed).length
|
||||
|
||||
for (const r of results) {
|
||||
console.log(`${r.passed ? "✅" : "❌"} ${r.test}`)
|
||||
}
|
||||
for (const r of results) {
|
||||
console.log(`${r.passed ? "✅" : "❌"} ${r.test}`)
|
||||
}
|
||||
|
||||
console.log(`\nTotal: ${passed} passed, ${failed} failed`)
|
||||
console.log(`\nTotal: ${passed} passed, ${failed} failed`)
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
if (failed > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -2,99 +2,109 @@ import "dotenv/config"
|
|||
import { openai } from "@ai-sdk/openai"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import {
|
||||
withSupermemory,
|
||||
supermemoryTools,
|
||||
searchMemoriesTool,
|
||||
addMemoryTool,
|
||||
type MemoryPromptData,
|
||||
withSupermemory,
|
||||
supermemoryTools,
|
||||
searchMemoriesTool,
|
||||
addMemoryTool,
|
||||
type MemoryPromptData,
|
||||
} from "@supermemory/tools/ai-sdk"
|
||||
|
||||
async function testMiddleware() {
|
||||
console.log("=== Middleware ===")
|
||||
console.log("=== Middleware ===")
|
||||
|
||||
// Basic wrapper
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123")
|
||||
console.log("✓ withSupermemory basic")
|
||||
// Basic wrapper
|
||||
const model = withSupermemory(openai("gpt-4"), "user-123")
|
||||
console.log("✓ withSupermemory basic")
|
||||
|
||||
// With addMemory option
|
||||
const modelWithAdd = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
addMemory: "always",
|
||||
})
|
||||
console.log("✓ withSupermemory with addMemory")
|
||||
// With addMemory option
|
||||
const modelWithAdd = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
addMemory: "always",
|
||||
})
|
||||
console.log("✓ withSupermemory with addMemory")
|
||||
|
||||
// With verbose logging
|
||||
const modelVerbose = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
verbose: true,
|
||||
})
|
||||
console.log("✓ withSupermemory with verbose")
|
||||
// With verbose logging
|
||||
const modelVerbose = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
verbose: true,
|
||||
})
|
||||
console.log("✓ withSupermemory with verbose")
|
||||
}
|
||||
|
||||
async function testSearchModes() {
|
||||
console.log("\n=== Search Modes ===")
|
||||
console.log("\n=== Search Modes ===")
|
||||
|
||||
const profileModel = withSupermemory(openai("gpt-4"), "user-123", { mode: "profile" })
|
||||
console.log("✓ mode: profile")
|
||||
const profileModel = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
mode: "profile",
|
||||
})
|
||||
console.log("✓ mode: profile")
|
||||
|
||||
const queryModel = withSupermemory(openai("gpt-4"), "user-123", { mode: "query" })
|
||||
console.log("✓ mode: query")
|
||||
const queryModel = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
mode: "query",
|
||||
})
|
||||
console.log("✓ mode: query")
|
||||
|
||||
const fullModel = withSupermemory(openai("gpt-4"), "user-123", { mode: "full" })
|
||||
console.log("✓ mode: full")
|
||||
const fullModel = withSupermemory(openai("gpt-4"), "user-123", {
|
||||
mode: "full",
|
||||
})
|
||||
console.log("✓ mode: full")
|
||||
}
|
||||
|
||||
async function testCustomPrompt() {
|
||||
console.log("\n=== Custom Prompt Template ===")
|
||||
console.log("\n=== Custom Prompt Template ===")
|
||||
|
||||
const anthropic = createAnthropic({ apiKey: "test-key" })
|
||||
const anthropic = createAnthropic({ apiKey: "test-key" })
|
||||
|
||||
const claudePrompt = (data: MemoryPromptData) =>
|
||||
`
|
||||
const claudePrompt = (data: MemoryPromptData) =>
|
||||
`
|
||||
<context>
|
||||
<user_profile>${data.userMemories}</user_profile>
|
||||
<relevant_memories>${data.generalSearchMemories}</relevant_memories>
|
||||
</context>
|
||||
`.trim()
|
||||
|
||||
const model = withSupermemory(anthropic("claude-3-sonnet-20240229"), "user-123", {
|
||||
mode: "full",
|
||||
promptTemplate: claudePrompt,
|
||||
})
|
||||
console.log("✓ Custom prompt template")
|
||||
const model = withSupermemory(
|
||||
anthropic("claude-3-sonnet-20240229"),
|
||||
"user-123",
|
||||
{
|
||||
mode: "full",
|
||||
promptTemplate: claudePrompt,
|
||||
},
|
||||
)
|
||||
console.log("✓ Custom prompt template")
|
||||
}
|
||||
|
||||
async function testTools() {
|
||||
console.log("\n=== Memory Tools ===")
|
||||
console.log("\n=== Memory Tools ===")
|
||||
|
||||
// All tools
|
||||
const tools = supermemoryTools("YOUR_API_KEY")
|
||||
console.log("✓ supermemoryTools")
|
||||
// All tools
|
||||
const tools = supermemoryTools("YOUR_API_KEY")
|
||||
console.log("✓ supermemoryTools")
|
||||
|
||||
// Individual tools
|
||||
const searchTool = searchMemoriesTool("API_KEY", { projectId: "personal" })
|
||||
console.log("✓ searchMemoriesTool")
|
||||
// Individual tools
|
||||
const searchTool = searchMemoriesTool("API_KEY", { projectId: "personal" })
|
||||
console.log("✓ searchMemoriesTool")
|
||||
|
||||
const addTool = addMemoryTool("API_KEY")
|
||||
console.log("✓ addMemoryTool")
|
||||
const addTool = addMemoryTool("API_KEY")
|
||||
console.log("✓ addMemoryTool")
|
||||
|
||||
// Combined
|
||||
const toolsObj = {
|
||||
searchMemories: searchTool,
|
||||
addMemory: addTool,
|
||||
}
|
||||
console.log("✓ Combined tools object")
|
||||
// Combined
|
||||
const toolsObj = {
|
||||
searchMemories: searchTool,
|
||||
addMemory: addTool,
|
||||
}
|
||||
console.log("✓ Combined tools object")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("AI SDK Integration Tests")
|
||||
console.log("========================\n")
|
||||
console.log("AI SDK Integration Tests")
|
||||
console.log("========================\n")
|
||||
|
||||
await testMiddleware()
|
||||
await testSearchModes()
|
||||
await testCustomPrompt()
|
||||
await testTools()
|
||||
await testMiddleware()
|
||||
await testSearchModes()
|
||||
await testCustomPrompt()
|
||||
await testTools()
|
||||
|
||||
console.log("\n========================")
|
||||
console.log("✅ All AI SDK tests passed!")
|
||||
console.log("\n========================")
|
||||
console.log("✅ All AI SDK tests passed!")
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -2,100 +2,104 @@ import "dotenv/config"
|
|||
import { createClaudeMemoryTool } from "@supermemory/tools/claude-memory"
|
||||
|
||||
async function testConfiguration() {
|
||||
console.log("=== Configuration ===")
|
||||
console.log("=== Configuration ===")
|
||||
|
||||
// Basic
|
||||
const tool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "my-app",
|
||||
})
|
||||
console.log("✓ Basic config")
|
||||
// Basic
|
||||
const tool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "my-app",
|
||||
})
|
||||
console.log("✓ Basic config")
|
||||
|
||||
// Full options
|
||||
const toolFull = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "my-app",
|
||||
containerTags: ["user-123", "project-alpha"],
|
||||
memoryContainerTag: "my_memory_prefix",
|
||||
})
|
||||
console.log("✓ Full config")
|
||||
// Full options
|
||||
const toolFull = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: "my-app",
|
||||
containerTags: ["user-123", "project-alpha"],
|
||||
memoryContainerTag: "my_memory_prefix",
|
||||
})
|
||||
console.log("✓ Full config")
|
||||
|
||||
return tool
|
||||
return tool
|
||||
}
|
||||
|
||||
async function testMethods(tool: ReturnType<typeof createClaudeMemoryTool>) {
|
||||
console.log("\n=== Methods ===")
|
||||
console.log("\n=== Methods ===")
|
||||
|
||||
console.log(`✓ handleCommand exists: ${typeof tool.handleCommand === "function"}`)
|
||||
console.log(
|
||||
`✓ handleCommandForToolResult exists: ${typeof tool.handleCommandForToolResult === "function"}`
|
||||
)
|
||||
console.log(
|
||||
`✓ handleCommand exists: ${typeof tool.handleCommand === "function"}`,
|
||||
)
|
||||
console.log(
|
||||
`✓ handleCommandForToolResult exists: ${typeof tool.handleCommandForToolResult === "function"}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function testCommands(tool: ReturnType<typeof createClaudeMemoryTool>) {
|
||||
console.log("\n=== Commands ===")
|
||||
console.log("\n=== Commands ===")
|
||||
|
||||
// View (list)
|
||||
const listResult = await tool.handleCommand({
|
||||
command: "view",
|
||||
path: "/memories/",
|
||||
})
|
||||
console.log(`✓ view (list): ${listResult.success}`)
|
||||
// View (list)
|
||||
const listResult = await tool.handleCommand({
|
||||
command: "view",
|
||||
path: "/memories/",
|
||||
})
|
||||
console.log(`✓ view (list): ${listResult.success}`)
|
||||
|
||||
// Create
|
||||
const createResult = await tool.handleCommand({
|
||||
command: "create",
|
||||
path: "/memories/test-file.txt",
|
||||
file_text: "User prefers dark mode\nFavorite language: TypeScript",
|
||||
})
|
||||
console.log(`✓ create: ${createResult.success}`)
|
||||
// Create
|
||||
const createResult = await tool.handleCommand({
|
||||
command: "create",
|
||||
path: "/memories/test-file.txt",
|
||||
file_text: "User prefers dark mode\nFavorite language: TypeScript",
|
||||
})
|
||||
console.log(`✓ create: ${createResult.success}`)
|
||||
|
||||
// Wait for indexing
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
// Wait for indexing
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
|
||||
// str_replace
|
||||
const replaceResult = await tool.handleCommand({
|
||||
command: "str_replace",
|
||||
path: "/memories/test-file.txt",
|
||||
old_str: "dark mode",
|
||||
new_str: "light mode",
|
||||
})
|
||||
console.log(`✓ str_replace: ${replaceResult.success}`)
|
||||
// str_replace
|
||||
const replaceResult = await tool.handleCommand({
|
||||
command: "str_replace",
|
||||
path: "/memories/test-file.txt",
|
||||
old_str: "dark mode",
|
||||
new_str: "light mode",
|
||||
})
|
||||
console.log(`✓ str_replace: ${replaceResult.success}`)
|
||||
|
||||
// Delete
|
||||
const deleteResult = await tool.handleCommand({
|
||||
command: "delete",
|
||||
path: "/memories/test-file.txt",
|
||||
})
|
||||
console.log(`✓ delete: ${deleteResult.success}`)
|
||||
// Delete
|
||||
const deleteResult = await tool.handleCommand({
|
||||
command: "delete",
|
||||
path: "/memories/test-file.txt",
|
||||
})
|
||||
console.log(`✓ delete: ${deleteResult.success}`)
|
||||
}
|
||||
|
||||
async function testToolResultFormat(tool: ReturnType<typeof createClaudeMemoryTool>) {
|
||||
console.log("\n=== Tool Result Format ===")
|
||||
async function testToolResultFormat(
|
||||
tool: ReturnType<typeof createClaudeMemoryTool>,
|
||||
) {
|
||||
console.log("\n=== Tool Result Format ===")
|
||||
|
||||
const toolResult = await tool.handleCommandForToolResult(
|
||||
{ command: "view", path: "/memories/" },
|
||||
"test-tool-id-123"
|
||||
)
|
||||
const toolResult = await tool.handleCommandForToolResult(
|
||||
{ command: "view", path: "/memories/" },
|
||||
"test-tool-id-123",
|
||||
)
|
||||
|
||||
const hasCorrectStructure =
|
||||
toolResult.type === "tool_result" &&
|
||||
toolResult.tool_use_id === "test-tool-id-123" &&
|
||||
typeof toolResult.content === "string" &&
|
||||
typeof toolResult.is_error === "boolean"
|
||||
const hasCorrectStructure =
|
||||
toolResult.type === "tool_result" &&
|
||||
toolResult.tool_use_id === "test-tool-id-123" &&
|
||||
typeof toolResult.content === "string" &&
|
||||
typeof toolResult.is_error === "boolean"
|
||||
|
||||
console.log(`✓ Tool result structure: ${hasCorrectStructure}`)
|
||||
console.log(`✓ Tool result structure: ${hasCorrectStructure}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Claude Memory Tool Tests")
|
||||
console.log("========================\n")
|
||||
console.log("Claude Memory Tool Tests")
|
||||
console.log("========================\n")
|
||||
|
||||
const tool = await testConfiguration()
|
||||
await testMethods(tool)
|
||||
await testCommands(tool)
|
||||
await testToolResultFormat(tool)
|
||||
const tool = await testConfiguration()
|
||||
await testMethods(tool)
|
||||
await testCommands(tool)
|
||||
await testToolResultFormat(tool)
|
||||
|
||||
console.log("\n========================")
|
||||
console.log("✅ All Claude Memory tests passed!")
|
||||
console.log("\n========================")
|
||||
console.log("✅ All Claude Memory tests passed!")
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -5,76 +5,82 @@ import { withSupermemory } from "@supermemory/tools/openai"
|
|||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
|
||||
|
||||
async function testOpenAIWrapper() {
|
||||
console.log("=== OpenAI SDK Wrapper ===")
|
||||
console.log("=== OpenAI SDK Wrapper ===")
|
||||
|
||||
if (!OPENAI_API_KEY) {
|
||||
console.log("⚠ OPENAI_API_KEY not set, skipping live tests")
|
||||
return false
|
||||
}
|
||||
if (!OPENAI_API_KEY) {
|
||||
console.log("⚠ OPENAI_API_KEY not set, skipping live tests")
|
||||
return false
|
||||
}
|
||||
|
||||
const openai = new OpenAI()
|
||||
const openai = new OpenAI()
|
||||
|
||||
// Basic wrapper
|
||||
const client = withSupermemory(openai, "docs-test-openai")
|
||||
console.log("✓ withSupermemory basic")
|
||||
// Basic wrapper
|
||||
const client = withSupermemory(openai, "docs-test-openai")
|
||||
console.log("✓ withSupermemory basic")
|
||||
|
||||
// With options
|
||||
const clientWithOptions = withSupermemory(openai, "docs-test-openai", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
verbose: true,
|
||||
})
|
||||
console.log("✓ withSupermemory with options")
|
||||
// With options
|
||||
const clientWithOptions = withSupermemory(openai, "docs-test-openai", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
verbose: true,
|
||||
})
|
||||
console.log("✓ withSupermemory with options")
|
||||
}
|
||||
|
||||
async function testChatCompletion() {
|
||||
console.log("\n=== Chat Completion with Memory ===")
|
||||
console.log("\n=== Chat Completion with Memory ===")
|
||||
|
||||
if (!OPENAI_API_KEY) {
|
||||
console.log("⚠ OPENAI_API_KEY not set, skipping")
|
||||
return
|
||||
}
|
||||
if (!OPENAI_API_KEY) {
|
||||
console.log("⚠ OPENAI_API_KEY not set, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
const openai = new OpenAI()
|
||||
const client = withSupermemory(openai, "docs-test-openai", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
})
|
||||
const openai = new OpenAI()
|
||||
const client = withSupermemory(openai, "docs-test-openai", {
|
||||
mode: "full",
|
||||
addMemory: "always",
|
||||
})
|
||||
|
||||
// Add context
|
||||
const addResponse = await client.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Remember that my favorite color is blue." },
|
||||
],
|
||||
max_tokens: 100,
|
||||
})
|
||||
console.log("✓ Add context:", addResponse.choices[0]?.message.content?.substring(0, 50))
|
||||
// Add context
|
||||
const addResponse = await client.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Remember that my favorite color is blue." },
|
||||
],
|
||||
max_tokens: 100,
|
||||
})
|
||||
console.log(
|
||||
"✓ Add context:",
|
||||
addResponse.choices[0]?.message.content?.substring(0, 50),
|
||||
)
|
||||
|
||||
// Retrieve context
|
||||
const retrieveResponse = await client.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "What do you know about my preferences?" },
|
||||
],
|
||||
max_tokens: 100,
|
||||
})
|
||||
console.log("✓ Retrieve context:", retrieveResponse.choices[0]?.message.content?.substring(0, 50))
|
||||
// Retrieve context
|
||||
const retrieveResponse = await client.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "What do you know about my preferences?" },
|
||||
],
|
||||
max_tokens: 100,
|
||||
})
|
||||
console.log(
|
||||
"✓ Retrieve context:",
|
||||
retrieveResponse.choices[0]?.message.content?.substring(0, 50),
|
||||
)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("OpenAI SDK Integration Tests")
|
||||
console.log("============================\n")
|
||||
console.log("OpenAI SDK Integration Tests")
|
||||
console.log("============================\n")
|
||||
|
||||
const hasKey = await testOpenAIWrapper()
|
||||
if (hasKey !== false) {
|
||||
await testChatCompletion()
|
||||
}
|
||||
const hasKey = await testOpenAIWrapper()
|
||||
if (hasKey !== false) {
|
||||
await testChatCompletion()
|
||||
}
|
||||
|
||||
console.log("\n============================")
|
||||
console.log("✅ All OpenAI SDK tests passed!")
|
||||
console.log("\n============================")
|
||||
console.log("✅ All OpenAI SDK tests passed!")
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -5,142 +5,147 @@ import Supermemory, { toFile } from "supermemory"
|
|||
const client = new Supermemory()
|
||||
|
||||
async function testDocumentsCRUD() {
|
||||
console.log("\n=== Document CRUD Operations ===")
|
||||
console.log("\n=== Document CRUD Operations ===")
|
||||
|
||||
// Create
|
||||
const doc = await client.documents.add({
|
||||
content: "Test content for TypeScript SDK - " + Date.now(),
|
||||
})
|
||||
console.log("✓ documents.add:", doc.id)
|
||||
// Create
|
||||
const doc = await client.documents.add({
|
||||
content: "Test content for TypeScript SDK - " + Date.now(),
|
||||
})
|
||||
console.log("✓ documents.add:", doc.id)
|
||||
|
||||
// Read
|
||||
const fetched = await client.documents.get(doc.id)
|
||||
console.log("✓ documents.get:", fetched.id)
|
||||
// Read
|
||||
const fetched = await client.documents.get(doc.id)
|
||||
console.log("✓ documents.get:", fetched.id)
|
||||
|
||||
// Update
|
||||
const updated = await client.documents.update(doc.id, {
|
||||
content: "Updated content - " + Date.now(),
|
||||
})
|
||||
console.log("✓ documents.update:", updated.id)
|
||||
// Update
|
||||
const updated = await client.documents.update(doc.id, {
|
||||
content: "Updated content - " + Date.now(),
|
||||
})
|
||||
console.log("✓ documents.update:", updated.id)
|
||||
|
||||
// Wait for processing before delete
|
||||
await new Promise((r) => setTimeout(r, 10000))
|
||||
// Wait for processing before delete
|
||||
await new Promise((r) => setTimeout(r, 10000))
|
||||
|
||||
// Delete
|
||||
await client.documents.delete(doc.id)
|
||||
console.log("✓ documents.delete")
|
||||
// Delete
|
||||
await client.documents.delete(doc.id)
|
||||
console.log("✓ documents.delete")
|
||||
}
|
||||
|
||||
async function testBatchOperations() {
|
||||
console.log("\n=== Batch Operations ===")
|
||||
console.log("\n=== Batch Operations ===")
|
||||
|
||||
const batch = await client.documents.batchAdd({
|
||||
documents: [
|
||||
{ content: "Batch doc 1 - " + Date.now() },
|
||||
{ content: "Batch doc 2 - " + Date.now() },
|
||||
],
|
||||
})
|
||||
console.log("✓ documents.batchAdd:", batch)
|
||||
const batch = await client.documents.batchAdd({
|
||||
documents: [
|
||||
{ content: "Batch doc 1 - " + Date.now() },
|
||||
{ content: "Batch doc 2 - " + Date.now() },
|
||||
],
|
||||
})
|
||||
console.log("✓ documents.batchAdd:", batch)
|
||||
}
|
||||
|
||||
async function testSearch() {
|
||||
console.log("\n=== Search ===")
|
||||
console.log("\n=== Search ===")
|
||||
|
||||
const results = await client.search.execute({ q: "test content" })
|
||||
console.log("✓ search.execute:", results.results?.length ?? 0, "results")
|
||||
const results = await client.search.execute({ q: "test content" })
|
||||
console.log("✓ search.execute:", results.results?.length ?? 0, "results")
|
||||
}
|
||||
|
||||
async function testFileUploads() {
|
||||
console.log("\n=== File Uploads ===")
|
||||
console.log("\n=== File Uploads ===")
|
||||
|
||||
// Using fs.createReadStream
|
||||
const testFile = "/tmp/test-doc-upload.txt"
|
||||
fs.writeFileSync(testFile, "Test file content")
|
||||
await client.documents.uploadFile({ file: fs.createReadStream(testFile) })
|
||||
console.log("✓ uploadFile with fs.createReadStream")
|
||||
// Using fs.createReadStream
|
||||
const testFile = "/tmp/test-doc-upload.txt"
|
||||
fs.writeFileSync(testFile, "Test file content")
|
||||
await client.documents.uploadFile({ file: fs.createReadStream(testFile) })
|
||||
console.log("✓ uploadFile with fs.createReadStream")
|
||||
|
||||
// Using File API
|
||||
await client.documents.uploadFile({ file: new File(["my bytes"], "file") })
|
||||
console.log("✓ uploadFile with File API")
|
||||
// Using File API
|
||||
await client.documents.uploadFile({ file: new File(["my bytes"], "file") })
|
||||
console.log("✓ uploadFile with File API")
|
||||
|
||||
// Using toFile
|
||||
await client.documents.uploadFile({ file: await toFile(Buffer.from("my bytes"), "file") })
|
||||
console.log("✓ uploadFile with toFile")
|
||||
// Using toFile
|
||||
await client.documents.uploadFile({
|
||||
file: await toFile(Buffer.from("my bytes"), "file"),
|
||||
})
|
||||
console.log("✓ uploadFile with toFile")
|
||||
|
||||
fs.unlinkSync(testFile)
|
||||
fs.unlinkSync(testFile)
|
||||
}
|
||||
|
||||
async function testClientConfig() {
|
||||
console.log("\n=== Client Configuration ===")
|
||||
console.log("\n=== Client Configuration ===")
|
||||
|
||||
// Retries
|
||||
const clientWithRetries = new Supermemory({ maxRetries: 0 })
|
||||
console.log("✓ maxRetries config")
|
||||
// Retries
|
||||
const clientWithRetries = new Supermemory({ maxRetries: 0 })
|
||||
console.log("✓ maxRetries config")
|
||||
|
||||
// Timeout
|
||||
const clientWithTimeout = new Supermemory({ timeout: 20 * 1000 })
|
||||
console.log("✓ timeout config")
|
||||
// Timeout
|
||||
const clientWithTimeout = new Supermemory({ timeout: 20 * 1000 })
|
||||
console.log("✓ timeout config")
|
||||
|
||||
// Per-request options
|
||||
await client.add({ content: "Test - " + Date.now() }, { maxRetries: 5, timeout: 5000 })
|
||||
console.log("✓ per-request options")
|
||||
// Per-request options
|
||||
await client.add(
|
||||
{ content: "Test - " + Date.now() },
|
||||
{ maxRetries: 5, timeout: 5000 },
|
||||
)
|
||||
console.log("✓ per-request options")
|
||||
}
|
||||
|
||||
async function testRawResponse() {
|
||||
console.log("\n=== Raw Response ===")
|
||||
console.log("\n=== Raw Response ===")
|
||||
|
||||
const response = await client.documents
|
||||
.add({ content: "Test - " + Date.now() })
|
||||
.asResponse()
|
||||
console.log("✓ .asResponse() status:", response.status)
|
||||
const response = await client.documents
|
||||
.add({ content: "Test - " + Date.now() })
|
||||
.asResponse()
|
||||
console.log("✓ .asResponse() status:", response.status)
|
||||
|
||||
const { data, response: raw } = await client.documents
|
||||
.add({ content: "Test - " + Date.now() })
|
||||
.withResponse()
|
||||
console.log("✓ .withResponse() data:", data.id, "status:", raw.status)
|
||||
const { data, response: raw } = await client.documents
|
||||
.add({ content: "Test - " + Date.now() })
|
||||
.withResponse()
|
||||
console.log("✓ .withResponse() data:", data.id, "status:", raw.status)
|
||||
}
|
||||
|
||||
async function testTypes() {
|
||||
console.log("\n=== Types ===")
|
||||
console.log("\n=== Types ===")
|
||||
|
||||
const params: Supermemory.AddParams = {
|
||||
content: "Typed content",
|
||||
}
|
||||
const response: Supermemory.AddResponse = await client.add(params)
|
||||
console.log("✓ TypeScript types work:", response.id)
|
||||
const params: Supermemory.AddParams = {
|
||||
content: "Typed content",
|
||||
}
|
||||
const response: Supermemory.AddResponse = await client.add(params)
|
||||
console.log("✓ TypeScript types work:", response.id)
|
||||
}
|
||||
|
||||
async function testErrorHandling() {
|
||||
console.log("\n=== Error Handling ===")
|
||||
console.log("\n=== Error Handling ===")
|
||||
|
||||
const response = await client.documents
|
||||
.add({ content: "Test - " + Date.now() })
|
||||
.catch(async (err) => {
|
||||
if (err instanceof Supermemory.APIError) {
|
||||
console.log("Caught APIError:", err.status, err.name)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
return null
|
||||
})
|
||||
console.log("✓ Error handling pattern works:", response?.id)
|
||||
const response = await client.documents
|
||||
.add({ content: "Test - " + Date.now() })
|
||||
.catch(async (err) => {
|
||||
if (err instanceof Supermemory.APIError) {
|
||||
console.log("Caught APIError:", err.status, err.name)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
return null
|
||||
})
|
||||
console.log("✓ Error handling pattern works:", response?.id)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("TypeScript SDK Tests")
|
||||
console.log("====================")
|
||||
console.log("TypeScript SDK Tests")
|
||||
console.log("====================")
|
||||
|
||||
await testDocumentsCRUD()
|
||||
await testBatchOperations()
|
||||
await testSearch()
|
||||
await testFileUploads()
|
||||
await testClientConfig()
|
||||
await testRawResponse()
|
||||
await testTypes()
|
||||
await testErrorHandling()
|
||||
await testDocumentsCRUD()
|
||||
await testBatchOperations()
|
||||
await testSearch()
|
||||
await testFileUploads()
|
||||
await testClientConfig()
|
||||
await testRawResponse()
|
||||
await testTypes()
|
||||
await testErrorHandling()
|
||||
|
||||
console.log("\n====================")
|
||||
console.log("✅ All TypeScript SDK tests passed!")
|
||||
console.log("\n====================")
|
||||
console.log("✅ All TypeScript SDK tests passed!")
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -4,107 +4,115 @@ import Supermemory from "supermemory"
|
|||
const client = new Supermemory()
|
||||
|
||||
async function testSearchModes() {
|
||||
console.log("=== Search Modes ===")
|
||||
console.log("=== Search Modes ===")
|
||||
|
||||
// Hybrid: memories + document chunks (recommended)
|
||||
const hybridResults = await client.search.memories({
|
||||
q: "quarterly goals",
|
||||
containerTag: "user_123",
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
console.log("✓ hybrid search:", hybridResults.results.length, "results")
|
||||
// Hybrid: memories + document chunks (recommended)
|
||||
const hybridResults = await client.search.memories({
|
||||
q: "quarterly goals",
|
||||
containerTag: "user_123",
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
console.log("✓ hybrid search:", hybridResults.results.length, "results")
|
||||
|
||||
// Memories only
|
||||
const memoriesResults = await client.search.memories({
|
||||
q: "user preferences",
|
||||
containerTag: "user_123",
|
||||
searchMode: "memories",
|
||||
})
|
||||
console.log("✓ memories search:", memoriesResults.results.length, "results")
|
||||
// Memories only
|
||||
const memoriesResults = await client.search.memories({
|
||||
q: "user preferences",
|
||||
containerTag: "user_123",
|
||||
searchMode: "memories",
|
||||
})
|
||||
console.log("✓ memories search:", memoriesResults.results.length, "results")
|
||||
}
|
||||
|
||||
async function testFiltering() {
|
||||
console.log("\n=== Filtering ===")
|
||||
console.log("\n=== Filtering ===")
|
||||
|
||||
// Basic containerTag filter
|
||||
const results = await client.search.memories({
|
||||
q: "project updates",
|
||||
containerTag: "user_123",
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
console.log("✓ containerTag filter:", results.results.length, "results")
|
||||
// Basic containerTag filter
|
||||
const results = await client.search.memories({
|
||||
q: "project updates",
|
||||
containerTag: "user_123",
|
||||
searchMode: "hybrid",
|
||||
})
|
||||
console.log("✓ containerTag filter:", results.results.length, "results")
|
||||
|
||||
// Metadata-based filtering
|
||||
const filteredResults = await client.search.memories({
|
||||
q: "meeting notes",
|
||||
containerTag: "user_123",
|
||||
filters: {
|
||||
AND: [
|
||||
{ key: "type", value: "meeting" },
|
||||
{ key: "year", value: "2024" },
|
||||
],
|
||||
},
|
||||
})
|
||||
console.log("✓ metadata filter:", filteredResults.results.length, "results")
|
||||
// Metadata-based filtering
|
||||
const filteredResults = await client.search.memories({
|
||||
q: "meeting notes",
|
||||
containerTag: "user_123",
|
||||
filters: {
|
||||
AND: [
|
||||
{ key: "type", value: "meeting" },
|
||||
{ key: "year", value: "2024" },
|
||||
],
|
||||
},
|
||||
})
|
||||
console.log("✓ metadata filter:", filteredResults.results.length, "results")
|
||||
}
|
||||
|
||||
async function testReranking() {
|
||||
console.log("\n=== Reranking ===")
|
||||
console.log("\n=== Reranking ===")
|
||||
|
||||
const results = await client.search.memories({
|
||||
q: "complex technical question",
|
||||
containerTag: "user_123",
|
||||
rerank: true,
|
||||
})
|
||||
console.log("✓ reranking:", results.results.length, "results")
|
||||
const results = await client.search.memories({
|
||||
q: "complex technical question",
|
||||
containerTag: "user_123",
|
||||
rerank: true,
|
||||
})
|
||||
console.log("✓ reranking:", results.results.length, "results")
|
||||
}
|
||||
|
||||
async function testThreshold() {
|
||||
console.log("\n=== Threshold ===")
|
||||
console.log("\n=== Threshold ===")
|
||||
|
||||
const broadResults = await client.search.memories({
|
||||
q: "test query",
|
||||
threshold: 0.3,
|
||||
})
|
||||
console.log("✓ broad threshold (0.3):", broadResults.results.length, "results")
|
||||
const broadResults = await client.search.memories({
|
||||
q: "test query",
|
||||
threshold: 0.3,
|
||||
})
|
||||
console.log(
|
||||
"✓ broad threshold (0.3):",
|
||||
broadResults.results.length,
|
||||
"results",
|
||||
)
|
||||
|
||||
const preciseResults = await client.search.memories({
|
||||
q: "test query",
|
||||
threshold: 0.8,
|
||||
})
|
||||
console.log("✓ precise threshold (0.8):", preciseResults.results.length, "results")
|
||||
const preciseResults = await client.search.memories({
|
||||
q: "test query",
|
||||
threshold: 0.8,
|
||||
})
|
||||
console.log(
|
||||
"✓ precise threshold (0.8):",
|
||||
preciseResults.results.length,
|
||||
"results",
|
||||
)
|
||||
}
|
||||
|
||||
async function testChatbotContext() {
|
||||
console.log("\n=== Chatbot Context Pattern ===")
|
||||
console.log("\n=== Chatbot Context Pattern ===")
|
||||
|
||||
async function getContext(userId: string, message: string) {
|
||||
const results = await client.search.memories({
|
||||
q: message,
|
||||
containerTag: userId,
|
||||
searchMode: "hybrid",
|
||||
threshold: 0.6,
|
||||
limit: 5,
|
||||
})
|
||||
return results.results.map((r) => r.memory || r.chunk).join("\n\n")
|
||||
}
|
||||
async function getContext(userId: string, message: string) {
|
||||
const results = await client.search.memories({
|
||||
q: message,
|
||||
containerTag: userId,
|
||||
searchMode: "hybrid",
|
||||
threshold: 0.6,
|
||||
limit: 5,
|
||||
})
|
||||
return results.results.map((r) => r.memory || r.chunk).join("\n\n")
|
||||
}
|
||||
|
||||
const context = await getContext("user_123", "What are the project updates?")
|
||||
console.log("✓ chatbot context:", context.length, "chars")
|
||||
const context = await getContext("user_123", "What are the project updates?")
|
||||
console.log("✓ chatbot context:", context.length, "chars")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Search Tests")
|
||||
console.log("============\n")
|
||||
console.log("Search Tests")
|
||||
console.log("============\n")
|
||||
|
||||
await testSearchModes()
|
||||
await testFiltering()
|
||||
await testReranking()
|
||||
await testThreshold()
|
||||
await testChatbotContext()
|
||||
await testSearchModes()
|
||||
await testFiltering()
|
||||
await testReranking()
|
||||
await testThreshold()
|
||||
await testChatbotContext()
|
||||
|
||||
console.log("\n============")
|
||||
console.log("✅ All search tests passed!")
|
||||
console.log("\n============")
|
||||
console.log("✅ All search tests passed!")
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -5,58 +5,62 @@ const client = new Supermemory()
|
|||
const USER_ID = "docs-test-profiles"
|
||||
|
||||
async function testBasicProfile() {
|
||||
console.log("=== Basic Profile ===")
|
||||
console.log("=== Basic Profile ===")
|
||||
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: "What are my preferences?",
|
||||
})
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: "What are my preferences?",
|
||||
})
|
||||
|
||||
console.log("✓ Static profile:", profile.profile.static.length, "items")
|
||||
console.log("✓ Dynamic profile:", profile.profile.dynamic.length, "items")
|
||||
console.log("✓ Search results:", profile.searchResults.results.length, "items")
|
||||
console.log("✓ Static profile:", profile.profile.static.length, "items")
|
||||
console.log("✓ Dynamic profile:", profile.profile.dynamic.length, "items")
|
||||
console.log(
|
||||
"✓ Search results:",
|
||||
profile.searchResults.results.length,
|
||||
"items",
|
||||
)
|
||||
}
|
||||
|
||||
async function testProfileWithMemories() {
|
||||
console.log("\n=== Profile with Memory Context ===")
|
||||
console.log("\n=== Profile with Memory Context ===")
|
||||
|
||||
// First add some memories
|
||||
await client.add({
|
||||
content: "User prefers dark mode for all applications",
|
||||
containerTag: USER_ID,
|
||||
})
|
||||
await client.add({
|
||||
content: "User is learning TypeScript and Rust",
|
||||
containerTag: USER_ID,
|
||||
})
|
||||
// First add some memories
|
||||
await client.add({
|
||||
content: "User prefers dark mode for all applications",
|
||||
containerTag: USER_ID,
|
||||
})
|
||||
await client.add({
|
||||
content: "User is learning TypeScript and Rust",
|
||||
containerTag: USER_ID,
|
||||
})
|
||||
|
||||
// Wait a bit for indexing
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
// Wait a bit for indexing
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
|
||||
// Now get profile with search
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: "What programming languages does the user know?",
|
||||
})
|
||||
// Now get profile with search
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: "What programming languages does the user know?",
|
||||
})
|
||||
|
||||
console.log("✓ Profile retrieved with memories")
|
||||
console.log(" Static:", profile.profile.static.slice(0, 2))
|
||||
console.log(" Dynamic:", profile.profile.dynamic.slice(0, 2))
|
||||
console.log("✓ Profile retrieved with memories")
|
||||
console.log(" Static:", profile.profile.static.slice(0, 2))
|
||||
console.log(" Dynamic:", profile.profile.dynamic.slice(0, 2))
|
||||
}
|
||||
|
||||
async function testBuildingContext() {
|
||||
console.log("\n=== Building LLM Context ===")
|
||||
console.log("\n=== Building LLM Context ===")
|
||||
|
||||
const conversation = [
|
||||
{ role: "user", content: "What theme should I use for my IDE?" },
|
||||
]
|
||||
const conversation = [
|
||||
{ role: "user", content: "What theme should I use for my IDE?" },
|
||||
]
|
||||
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: conversation.at(-1)!.content,
|
||||
})
|
||||
const profile = await client.profile({
|
||||
containerTag: USER_ID,
|
||||
q: conversation.at(-1)!.content,
|
||||
})
|
||||
|
||||
const context = `User Profile:
|
||||
const context = `User Profile:
|
||||
${profile.profile.static.join("\n")}
|
||||
|
||||
Recent Context:
|
||||
|
|
@ -65,25 +69,25 @@ ${profile.profile.dynamic.join("\n")}
|
|||
Relevant Memories:
|
||||
${profile.searchResults.results.map((r) => r.content).join("\n")}`
|
||||
|
||||
console.log("✓ Built context:", context.length, "chars")
|
||||
console.log("✓ Built context:", context.length, "chars")
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: `Use this context about the user:\n${context}` },
|
||||
...conversation,
|
||||
]
|
||||
console.log("✓ Messages ready for LLM:", messages.length, "messages")
|
||||
const messages = [
|
||||
{ role: "system", content: `Use this context about the user:\n${context}` },
|
||||
...conversation,
|
||||
]
|
||||
console.log("✓ Messages ready for LLM:", messages.length, "messages")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("User Profiles Tests")
|
||||
console.log("===================\n")
|
||||
console.log("User Profiles Tests")
|
||||
console.log("===================\n")
|
||||
|
||||
await testBasicProfile()
|
||||
await testProfileWithMemories()
|
||||
await testBuildingContext()
|
||||
await testBasicProfile()
|
||||
await testProfileWithMemories()
|
||||
await testBuildingContext()
|
||||
|
||||
console.log("\n===================")
|
||||
console.log("✅ All user profile tests passed!")
|
||||
console.log("\n===================")
|
||||
console.log("✅ All user profile tests passed!")
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -127,9 +127,8 @@ export function useForceSimulation(
|
|||
|
||||
// Update edges
|
||||
if (edges.length > 0) {
|
||||
const linkForce = simulationRef.current.force<
|
||||
d3.ForceLink<GraphNode, GraphEdge>
|
||||
>("link")
|
||||
const linkForce =
|
||||
simulationRef.current.force<d3.ForceLink<GraphNode, GraphEdge>>("link")
|
||||
if (linkForce) {
|
||||
linkForce.links(edges)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,16 @@ import type {
|
|||
export function useGraphData(
|
||||
data: DocumentsResponse | null,
|
||||
selectedSpace: string,
|
||||
nodePositions: Map<string, { x: number; y: number; parentDocId?: string; offsetX?: number; offsetY?: number }>,
|
||||
nodePositions: Map<
|
||||
string,
|
||||
{
|
||||
x: number
|
||||
y: number
|
||||
parentDocId?: string
|
||||
offsetX?: number
|
||||
offsetY?: number
|
||||
}
|
||||
>,
|
||||
draggingNodeId: string | null,
|
||||
memoryLimit?: number,
|
||||
maxNodes?: number,
|
||||
|
|
@ -60,35 +69,34 @@ export function useGraphData(
|
|||
})
|
||||
|
||||
// Filter by space and prepare documents
|
||||
let processedDocs = sortedDocs
|
||||
.map((doc) => {
|
||||
let memories =
|
||||
selectedSpace === "all"
|
||||
? doc.memoryEntries
|
||||
: doc.memoryEntries.filter(
|
||||
(memory) =>
|
||||
(memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
|
||||
selectedSpace,
|
||||
)
|
||||
let processedDocs = sortedDocs.map((doc) => {
|
||||
let memories =
|
||||
selectedSpace === "all"
|
||||
? doc.memoryEntries
|
||||
: doc.memoryEntries.filter(
|
||||
(memory) =>
|
||||
(memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
|
||||
selectedSpace,
|
||||
)
|
||||
|
||||
// Sort memories by relevance score (if available) or recency
|
||||
memories = memories.sort((a, b) => {
|
||||
// Prioritize sourceRelevanceScore if available
|
||||
if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
|
||||
return b.sourceRelevanceScore - a.sourceRelevanceScore // Higher score first
|
||||
}
|
||||
// Fall back to most recent
|
||||
const dateA = new Date(a.updatedAt || a.createdAt).getTime()
|
||||
const dateB = new Date(b.updatedAt || b.createdAt).getTime()
|
||||
return dateB - dateA // Most recent first
|
||||
})
|
||||
|
||||
return {
|
||||
...doc,
|
||||
memoryEntries: memories,
|
||||
// Sort memories by relevance score (if available) or recency
|
||||
memories = memories.sort((a, b) => {
|
||||
// Prioritize sourceRelevanceScore if available
|
||||
if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
|
||||
return b.sourceRelevanceScore - a.sourceRelevanceScore // Higher score first
|
||||
}
|
||||
// Fall back to most recent
|
||||
const dateA = new Date(a.updatedAt || a.createdAt).getTime()
|
||||
const dateB = new Date(b.updatedAt || b.createdAt).getTime()
|
||||
return dateB - dateA // Most recent first
|
||||
})
|
||||
|
||||
return {
|
||||
...doc,
|
||||
memoryEntries: memories,
|
||||
}
|
||||
})
|
||||
|
||||
// Apply maxNodes limit using Option B (dynamic cap per document)
|
||||
if (maxNodes && maxNodes > 0) {
|
||||
const totalDocs = processedDocs.length
|
||||
|
|
@ -112,37 +120,57 @@ export function useGraphData(
|
|||
// If we still have budget left, distribute remaining nodes to first docs
|
||||
let remainingBudget = maxNodes - totalNodes
|
||||
if (remainingBudget > 0) {
|
||||
for (let i = 0; i < processedDocs.length && remainingBudget > 0; i++) {
|
||||
for (
|
||||
let i = 0;
|
||||
i < processedDocs.length && remainingBudget > 0;
|
||||
i++
|
||||
) {
|
||||
const doc = processedDocs[i]
|
||||
if (!doc) continue
|
||||
const originalDoc = sortedDocs.find(d => d.id === doc.id)
|
||||
const originalDoc = sortedDocs.find((d) => d.id === doc.id)
|
||||
if (!originalDoc) continue
|
||||
|
||||
const currentMemCount = doc.memoryEntries.length
|
||||
const originalMemCount = originalDoc.memoryEntries.filter(
|
||||
m => selectedSpace === "all" ||
|
||||
(m.spaceContainerTag ?? m.spaceId ?? "default") === selectedSpace
|
||||
(m) =>
|
||||
selectedSpace === "all" ||
|
||||
(m.spaceContainerTag ?? m.spaceId ?? "default") ===
|
||||
selectedSpace,
|
||||
).length
|
||||
|
||||
// Can we add more memories to this doc?
|
||||
const canAdd = originalMemCount - currentMemCount
|
||||
if (canAdd > 0) {
|
||||
const toAdd = Math.min(canAdd, remainingBudget)
|
||||
const additionalMems = doc.memoryEntries.slice(0, currentMemCount + toAdd)
|
||||
const additionalMems = doc.memoryEntries.slice(
|
||||
0,
|
||||
currentMemCount + toAdd,
|
||||
)
|
||||
processedDocs[i] = {
|
||||
...doc,
|
||||
memoryEntries: originalDoc.memoryEntries
|
||||
.filter(m => selectedSpace === "all" ||
|
||||
(m.spaceContainerTag ?? m.spaceId ?? "default") === selectedSpace)
|
||||
.filter(
|
||||
(m) =>
|
||||
selectedSpace === "all" ||
|
||||
(m.spaceContainerTag ?? m.spaceId ?? "default") ===
|
||||
selectedSpace,
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
|
||||
if (
|
||||
a.sourceRelevanceScore != null &&
|
||||
b.sourceRelevanceScore != null
|
||||
) {
|
||||
return b.sourceRelevanceScore - a.sourceRelevanceScore
|
||||
}
|
||||
const dateA = new Date(a.updatedAt || a.createdAt).getTime()
|
||||
const dateB = new Date(b.updatedAt || b.createdAt).getTime()
|
||||
const dateA = new Date(
|
||||
a.updatedAt || a.createdAt,
|
||||
).getTime()
|
||||
const dateB = new Date(
|
||||
b.updatedAt || b.createdAt,
|
||||
).getTime()
|
||||
return dateB - dateA
|
||||
})
|
||||
.slice(0, currentMemCount + toAdd)
|
||||
.slice(0, currentMemCount + toAdd),
|
||||
}
|
||||
remainingBudget -= toAdd
|
||||
}
|
||||
|
|
@ -238,8 +266,7 @@ export function useGraphData(
|
|||
})
|
||||
|
||||
// Enhanced Layout with Space Separation
|
||||
const { centerX, centerY, clusterRadius } =
|
||||
LAYOUT_CONSTANTS
|
||||
const { centerX, centerY, clusterRadius } = LAYOUT_CONSTANTS
|
||||
|
||||
/* 1. Build DOCUMENT nodes with space-aware clustering */
|
||||
const documentNodes: GraphNode[] = []
|
||||
|
|
@ -255,8 +282,10 @@ export function useGraphData(
|
|||
|
||||
// Loose grid spacing - physics will organize it better
|
||||
const spacing = 200
|
||||
const defaultX = centerX + (col - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
|
||||
const defaultY = centerY + (row - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
|
||||
const defaultX =
|
||||
centerX + (col - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
|
||||
const defaultY =
|
||||
centerY + (row - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
|
||||
|
||||
const customPos = nodePositions.get(doc.id)
|
||||
|
||||
|
|
@ -294,7 +323,7 @@ export function useGraphData(
|
|||
// D3-force will handle collision avoidance and spacing dynamically
|
||||
|
||||
allNodes.push(...documentNodes)
|
||||
|
||||
|
||||
/* 3. Add memories around documents WITH doc-memory connections */
|
||||
documentNodes.forEach((docNode) => {
|
||||
const memoryNodeMap = new Map<string, GraphNode>()
|
||||
|
|
@ -318,9 +347,11 @@ export function useGraphData(
|
|||
|
||||
if (customMemPos) {
|
||||
// If memory was manually positioned and has stored offset relative to parent
|
||||
if (customMemPos.parentDocId === docNode.id &&
|
||||
if (
|
||||
customMemPos.parentDocId === docNode.id &&
|
||||
customMemPos.offsetX !== undefined &&
|
||||
customMemPos.offsetY !== undefined) {
|
||||
customMemPos.offsetY !== undefined
|
||||
) {
|
||||
// Apply the stored offset to the current document position
|
||||
finalMemX = docNode.x + customMemPos.offsetX
|
||||
finalMemY = docNode.y + customMemPos.offsetY
|
||||
|
|
@ -386,7 +417,8 @@ export function useGraphData(
|
|||
data.documents.forEach((doc) => {
|
||||
doc.memoryEntries.forEach((mem: MemoryEntry) => {
|
||||
// Support both new object structure and legacy array/single parent fields
|
||||
let parentRelations: Record<string, MemoryRelation> = (mem.memoryRelations ?? {}) as Record<string, MemoryRelation>
|
||||
let parentRelations: Record<string, MemoryRelation> =
|
||||
(mem.memoryRelations ?? {}) as Record<string, MemoryRelation>
|
||||
|
||||
if (
|
||||
mem.memoryRelations &&
|
||||
|
|
@ -436,4 +468,4 @@ export function useGraphData(
|
|||
|
||||
return { nodes: allNodes, edges: allEdges }
|
||||
}, [data, filteredDocuments, nodePositions, draggingNodeId, similarityEdges])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,16 @@ export function useGraphInteractions(
|
|||
nodeY: 0,
|
||||
})
|
||||
const [nodePositions, setNodePositions] = useState<
|
||||
Map<string, { x: number; y: number; parentDocId?: string; offsetX?: number; offsetY?: number }>
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
x: number
|
||||
y: number
|
||||
parentDocId?: string
|
||||
offsetX?: number
|
||||
offsetY?: number
|
||||
}
|
||||
>
|
||||
>(new Map())
|
||||
|
||||
// Touch gesture state
|
||||
|
|
@ -125,8 +134,11 @@ export function useGraphInteractions(
|
|||
// For memory nodes, find the parent document and store relative offset
|
||||
const memoryData = draggedNode.data as any // MemoryEntry type
|
||||
const parentDoc = nodes?.find(
|
||||
(n) => n.type === "document" &&
|
||||
(n.data as any).memoryEntries?.some((m: any) => m.id === memoryData.id)
|
||||
(n) =>
|
||||
n.type === "document" &&
|
||||
(n.data as any).memoryEntries?.some(
|
||||
(m: any) => m.id === memoryData.id,
|
||||
),
|
||||
)
|
||||
|
||||
if (parentDoc) {
|
||||
|
|
@ -140,7 +152,7 @@ export function useGraphInteractions(
|
|||
y: newY,
|
||||
parentDocId: parentDoc.id,
|
||||
offsetX,
|
||||
offsetY
|
||||
offsetY,
|
||||
}),
|
||||
)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -4,68 +4,68 @@
|
|||
* Shows the cleanest way to integrate Claude's memory tool with supermemory
|
||||
*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { createClaudeMemoryTool } from './claude-memory'
|
||||
import Anthropic from "@anthropic-ai/sdk"
|
||||
import { createClaudeMemoryTool } from "./claude-memory"
|
||||
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
})
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
|
||||
projectId: 'my-app',
|
||||
projectId: "my-app",
|
||||
})
|
||||
|
||||
async function chatWithMemory(userMessage: string) {
|
||||
// Send message to Claude with memory tool
|
||||
const response = await anthropic.beta.messages.create({
|
||||
model: 'claude-sonnet-4-5',
|
||||
max_tokens: 2048,
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
tools: [{ type: 'memory_20250818', name: 'memory' }],
|
||||
betas: ['context-management-2025-06-27'],
|
||||
})
|
||||
// Send message to Claude with memory tool
|
||||
const response = await anthropic.beta.messages.create({
|
||||
model: "claude-sonnet-4-5",
|
||||
max_tokens: 2048,
|
||||
messages: [{ role: "user", content: userMessage }],
|
||||
tools: [{ type: "memory_20250818", name: "memory" }],
|
||||
betas: ["context-management-2025-06-27"],
|
||||
})
|
||||
|
||||
// Handle any memory tool calls
|
||||
const toolResults = []
|
||||
for (const block of response.content) {
|
||||
if (block.type === 'tool_use' && block.name === 'memory') {
|
||||
const toolResult = await memoryTool.handleCommandForToolResult(
|
||||
block.input as any,
|
||||
block.id
|
||||
)
|
||||
toolResults.push(toolResult)
|
||||
}
|
||||
}
|
||||
// Handle any memory tool calls
|
||||
const toolResults = []
|
||||
for (const block of response.content) {
|
||||
if (block.type === "tool_use" && block.name === "memory") {
|
||||
const toolResult = await memoryTool.handleCommandForToolResult(
|
||||
block.input as any,
|
||||
block.id,
|
||||
)
|
||||
toolResults.push(toolResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Send tool results back to Claude if needed
|
||||
if (toolResults.length > 0) {
|
||||
const finalResponse = await anthropic.beta.messages.create({
|
||||
model: 'claude-sonnet-4-5',
|
||||
max_tokens: 2048,
|
||||
messages: [
|
||||
{ role: 'user', content: userMessage },
|
||||
{ role: 'assistant', content: response.content },
|
||||
{ role: 'user', content: toolResults },
|
||||
],
|
||||
tools: [{ type: 'memory_20250818', name: 'memory' }],
|
||||
betas: ['context-management-2025-06-27'],
|
||||
})
|
||||
// Send tool results back to Claude if needed
|
||||
if (toolResults.length > 0) {
|
||||
const finalResponse = await anthropic.beta.messages.create({
|
||||
model: "claude-sonnet-4-5",
|
||||
max_tokens: 2048,
|
||||
messages: [
|
||||
{ role: "user", content: userMessage },
|
||||
{ role: "assistant", content: response.content },
|
||||
{ role: "user", content: toolResults },
|
||||
],
|
||||
tools: [{ type: "memory_20250818", name: "memory" }],
|
||||
betas: ["context-management-2025-06-27"],
|
||||
})
|
||||
|
||||
return finalResponse
|
||||
}
|
||||
return finalResponse
|
||||
}
|
||||
|
||||
return response
|
||||
return response
|
||||
}
|
||||
|
||||
// Example usage
|
||||
async function example() {
|
||||
const response = await chatWithMemory(
|
||||
"Remember that I prefer React with TypeScript for my projects"
|
||||
)
|
||||
const response = await chatWithMemory(
|
||||
"Remember that I prefer React with TypeScript for my projects",
|
||||
)
|
||||
|
||||
console.log(response.content[0])
|
||||
console.log(response.content[0])
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
example()
|
||||
}
|
||||
example()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,13 @@ async function testResponses() {
|
|||
console.log("\n=== Testing Responses API ===")
|
||||
const response = await openaiWithSupermemory.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "user", content: "what's my favoritge color?" },
|
||||
],
|
||||
messages: [{ role: "user", content: "what's my favoritge color?" }],
|
||||
})
|
||||
|
||||
console.log("Response:", JSON.stringify(response.choices[0]?.message.content, null, 2))
|
||||
console.log(
|
||||
"Response:",
|
||||
JSON.stringify(response.choices[0]?.message.content, null, 2),
|
||||
)
|
||||
}
|
||||
|
||||
// Run tests
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ const result = await generateText({
|
|||
messages: [{ role: "user", content: "Where do i live?" }],
|
||||
})
|
||||
|
||||
console.log(result.text)
|
||||
console.log(result.text)
|
||||
|
|
|
|||
|
|
@ -4,257 +4,281 @@
|
|||
* Shows how to use the memory tool with the official Anthropic SDK
|
||||
*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { createClaudeMemoryTool } from './claude-memory'
|
||||
import 'dotenv/config'
|
||||
import Anthropic from "@anthropic-ai/sdk"
|
||||
import { createClaudeMemoryTool } from "./claude-memory"
|
||||
import "dotenv/config"
|
||||
|
||||
/**
|
||||
* Handle Claude's memory tool calls using the Anthropic SDK
|
||||
*/
|
||||
async function chatWithMemoryTool() {
|
||||
console.log('🤖 Anthropic SDK Example - Claude with Memory Tool')
|
||||
console.log('=' .repeat(50))
|
||||
console.log("🤖 Anthropic SDK Example - Claude with Memory Tool")
|
||||
console.log("=".repeat(50))
|
||||
|
||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY
|
||||
const SUPERMEMORY_API_KEY = process.env.SUPERMEMORY_API_KEY
|
||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY
|
||||
const SUPERMEMORY_API_KEY = process.env.SUPERMEMORY_API_KEY
|
||||
|
||||
if (!ANTHROPIC_API_KEY || !SUPERMEMORY_API_KEY) {
|
||||
console.error('❌ Missing required API keys:')
|
||||
console.error('- ANTHROPIC_API_KEY')
|
||||
console.error('- SUPERMEMORY_API_KEY')
|
||||
return
|
||||
}
|
||||
if (!ANTHROPIC_API_KEY || !SUPERMEMORY_API_KEY) {
|
||||
console.error("❌ Missing required API keys:")
|
||||
console.error("- ANTHROPIC_API_KEY")
|
||||
console.error("- SUPERMEMORY_API_KEY")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize Anthropic client
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: ANTHROPIC_API_KEY,
|
||||
})
|
||||
// Initialize Anthropic client
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: ANTHROPIC_API_KEY,
|
||||
})
|
||||
|
||||
// Initialize memory tool
|
||||
const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, {
|
||||
projectId: 'anthropic-sdk-demo',
|
||||
memoryContainerTag: 'claude_memory_anthropic',
|
||||
})
|
||||
// Initialize memory tool
|
||||
const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, {
|
||||
projectId: "anthropic-sdk-demo",
|
||||
memoryContainerTag: "claude_memory_anthropic",
|
||||
})
|
||||
|
||||
// Conversation messages
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: "Hi Claude! I'm working on a new React project using TypeScript and I want you to remember my preferences. Can you help me debug some code later?",
|
||||
},
|
||||
]
|
||||
// Conversation messages
|
||||
const messages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Hi Claude! I'm working on a new React project using TypeScript and I want you to remember my preferences. Can you help me debug some code later?",
|
||||
},
|
||||
]
|
||||
|
||||
console.log('💬 User:', messages[0].content)
|
||||
console.log('\n🔄 Sending to Claude with memory tool...')
|
||||
console.log("💬 User:", messages[0].content)
|
||||
console.log("\n🔄 Sending to Claude with memory tool...")
|
||||
|
||||
try {
|
||||
// Make initial request to Claude with memory tool
|
||||
const response = await anthropic.beta.messages.create({
|
||||
model: 'claude-sonnet-4-5',
|
||||
max_tokens: 2048,
|
||||
messages: messages,
|
||||
tools: [
|
||||
{
|
||||
type: 'memory_20250818',
|
||||
name: 'memory',
|
||||
},
|
||||
],
|
||||
betas: ['context-management-2025-06-27'],
|
||||
})
|
||||
try {
|
||||
// Make initial request to Claude with memory tool
|
||||
const response = await anthropic.beta.messages.create({
|
||||
model: "claude-sonnet-4-5",
|
||||
max_tokens: 2048,
|
||||
messages: messages,
|
||||
tools: [
|
||||
{
|
||||
type: "memory_20250818",
|
||||
name: "memory",
|
||||
},
|
||||
],
|
||||
betas: ["context-management-2025-06-27"],
|
||||
})
|
||||
|
||||
console.log('📥 Claude responded:')
|
||||
console.log("📥 Claude responded:")
|
||||
|
||||
// Process the response
|
||||
let toolResults: Anthropic.Messages.ToolResultBlockParam[] = []
|
||||
// Process the response
|
||||
const toolResults: Anthropic.Messages.ToolResultBlockParam[] = []
|
||||
|
||||
for (const block of response.content) {
|
||||
if (block.type === 'text') {
|
||||
console.log('💭', block.text)
|
||||
} else if (block.type === 'tool_use' && block.name === 'memory') {
|
||||
console.log('🔧 Claude is using memory tool:')
|
||||
console.log(' Command:', block.input.command)
|
||||
console.log(' Path:', block.input.path)
|
||||
for (const block of response.content) {
|
||||
if (block.type === "text") {
|
||||
console.log("💭", block.text)
|
||||
} else if (block.type === "tool_use" && block.name === "memory") {
|
||||
console.log("🔧 Claude is using memory tool:")
|
||||
console.log(" Command:", block.input.command)
|
||||
console.log(" Path:", block.input.path)
|
||||
|
||||
// Handle the memory tool call
|
||||
const memoryResult = await memoryTool.handleCommand(block.input as any)
|
||||
// Handle the memory tool call
|
||||
const memoryResult = await memoryTool.handleCommand(block.input as any)
|
||||
|
||||
const toolResult: Anthropic.Messages.ToolResultBlockParam = {
|
||||
type: 'tool_result',
|
||||
tool_use_id: block.id,
|
||||
content: memoryResult.success
|
||||
? memoryResult.content || 'Operation completed successfully'
|
||||
: `Error: ${memoryResult.error}`,
|
||||
is_error: !memoryResult.success,
|
||||
}
|
||||
const toolResult: Anthropic.Messages.ToolResultBlockParam = {
|
||||
type: "tool_result",
|
||||
tool_use_id: block.id,
|
||||
content: memoryResult.success
|
||||
? memoryResult.content || "Operation completed successfully"
|
||||
: `Error: ${memoryResult.error}`,
|
||||
is_error: !memoryResult.success,
|
||||
}
|
||||
|
||||
toolResults.push(toolResult)
|
||||
toolResults.push(toolResult)
|
||||
|
||||
console.log('📊 Memory operation result:', memoryResult.success ? '✅ Success' : '❌ Failed')
|
||||
if (memoryResult.content) {
|
||||
console.log('📄 Content preview:', memoryResult.content.substring(0, 100) + '...')
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
"📊 Memory operation result:",
|
||||
memoryResult.success ? "✅ Success" : "❌ Failed",
|
||||
)
|
||||
if (memoryResult.content) {
|
||||
console.log(
|
||||
"📄 Content preview:",
|
||||
memoryResult.content.substring(0, 100) + "...",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If Claude used memory tools, send the results back
|
||||
if (toolResults.length > 0) {
|
||||
console.log('\n🔄 Sending tool results back to Claude...')
|
||||
// If Claude used memory tools, send the results back
|
||||
if (toolResults.length > 0) {
|
||||
console.log("\n🔄 Sending tool results back to Claude...")
|
||||
|
||||
// Add Claude's response to conversation
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: response.content,
|
||||
})
|
||||
// Add Claude's response to conversation
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: response.content,
|
||||
})
|
||||
|
||||
// Add tool results
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: toolResults,
|
||||
})
|
||||
// Add tool results
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: toolResults,
|
||||
})
|
||||
|
||||
const finalResponse = await anthropic.beta.messages.create({
|
||||
model: 'claude-sonnet-4-5',
|
||||
max_tokens: 2048,
|
||||
messages: messages,
|
||||
tools: [
|
||||
{
|
||||
type: 'memory_20250818',
|
||||
name: 'memory',
|
||||
},
|
||||
],
|
||||
betas: ['context-management-2025-06-27'],
|
||||
})
|
||||
const finalResponse = await anthropic.beta.messages.create({
|
||||
model: "claude-sonnet-4-5",
|
||||
max_tokens: 2048,
|
||||
messages: messages,
|
||||
tools: [
|
||||
{
|
||||
type: "memory_20250818",
|
||||
name: "memory",
|
||||
},
|
||||
],
|
||||
betas: ["context-management-2025-06-27"],
|
||||
})
|
||||
|
||||
console.log('📥 Claude\'s final response:')
|
||||
console.log("📥 Claude's final response:")
|
||||
|
||||
for (const block of finalResponse.content) {
|
||||
if (block.type === 'text') {
|
||||
console.log('💭', block.text)
|
||||
} else if (block.type === 'tool_use' && block.name === 'memory') {
|
||||
console.log('🔧 Claude is using memory tool again:')
|
||||
console.log(' Command:', block.input.command)
|
||||
console.log(' Path:', block.input.path)
|
||||
for (const block of finalResponse.content) {
|
||||
if (block.type === "text") {
|
||||
console.log("💭", block.text)
|
||||
} else if (block.type === "tool_use" && block.name === "memory") {
|
||||
console.log("🔧 Claude is using memory tool again:")
|
||||
console.log(" Command:", block.input.command)
|
||||
console.log(" Path:", block.input.path)
|
||||
|
||||
// Handle additional memory tool calls
|
||||
const memoryResult = await memoryTool.handleCommand(block.input as any)
|
||||
console.log('📊 Memory operation result:', memoryResult.success ? '✅ Success' : '❌ Failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle additional memory tool calls
|
||||
const memoryResult = await memoryTool.handleCommand(
|
||||
block.input as any,
|
||||
)
|
||||
console.log(
|
||||
"📊 Memory operation result:",
|
||||
memoryResult.success ? "✅ Success" : "❌ Failed",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✨ Conversation completed!')
|
||||
console.log('\n📋 Usage statistics:')
|
||||
console.log('- Input tokens:', response.usage.input_tokens)
|
||||
console.log('- Output tokens:', response.usage.output_tokens)
|
||||
console.log('- Memory operations:', toolResults.length)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error)
|
||||
}
|
||||
console.log("\n✨ Conversation completed!")
|
||||
console.log("\n📋 Usage statistics:")
|
||||
console.log("- Input tokens:", response.usage.input_tokens)
|
||||
console.log("- Output tokens:", response.usage.output_tokens)
|
||||
console.log("- Memory operations:", toolResults.length)
|
||||
} catch (error) {
|
||||
console.error("❌ Error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test memory tool operations directly
|
||||
*/
|
||||
async function testMemoryOperations() {
|
||||
console.log('\n🧪 Testing Memory Operations Directly')
|
||||
console.log('=' .repeat(50))
|
||||
console.log("\n🧪 Testing Memory Operations Directly")
|
||||
console.log("=".repeat(50))
|
||||
|
||||
if (!process.env.SUPERMEMORY_API_KEY) {
|
||||
console.error('❌ SUPERMEMORY_API_KEY is required')
|
||||
return
|
||||
}
|
||||
if (!process.env.SUPERMEMORY_API_KEY) {
|
||||
console.error("❌ SUPERMEMORY_API_KEY is required")
|
||||
return
|
||||
}
|
||||
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
|
||||
projectId: 'direct-test',
|
||||
memoryContainerTag: 'claude_memory_direct',
|
||||
})
|
||||
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
|
||||
projectId: "direct-test",
|
||||
memoryContainerTag: "claude_memory_direct",
|
||||
})
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'View empty memory directory',
|
||||
command: { command: 'view' as const, path: '/memories' },
|
||||
},
|
||||
{
|
||||
name: 'Create user preferences file',
|
||||
command: {
|
||||
command: 'create' as const,
|
||||
path: '/memories/user-preferences.md',
|
||||
file_text: '# User Preferences\n\n- Prefers React with TypeScript\n- Likes clean, readable code\n- Uses functional programming style\n- Prefers ESLint and Prettier for code formatting',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Create project notes',
|
||||
command: {
|
||||
command: 'create' as const,
|
||||
path: '/memories/project-notes.txt',
|
||||
file_text: 'Current Project: React TypeScript App\n\nFeatures to implement:\n1. User authentication\n2. Dashboard with widgets\n3. Data visualization\n4. Export functionality\n\nTech stack:\n- React 18\n- TypeScript\n- Vite\n- Tailwind CSS',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'List directory contents',
|
||||
command: { command: 'view' as const, path: '/memories/' },
|
||||
},
|
||||
{
|
||||
name: 'Read user preferences',
|
||||
command: { command: 'view' as const, path: '/memories/user-preferences.md' },
|
||||
},
|
||||
{
|
||||
name: 'Update preferences (add VS Code)',
|
||||
command: {
|
||||
command: 'str_replace' as const,
|
||||
path: '/memories/user-preferences.md',
|
||||
old_str: '- Prefers ESLint and Prettier for code formatting',
|
||||
new_str: '- Prefers ESLint and Prettier for code formatting\n- Uses VS Code as primary editor\n- Likes GitHub Copilot for code completion',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Insert new task in project notes',
|
||||
command: {
|
||||
command: 'insert' as const,
|
||||
path: '/memories/project-notes.txt',
|
||||
insert_line: 6,
|
||||
insert_text: '5. Unit testing with Jest and React Testing Library',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Read updated project notes',
|
||||
command: { command: 'view' as const, path: '/memories/project-notes.txt', view_range: [4, 8] },
|
||||
},
|
||||
]
|
||||
const testCases = [
|
||||
{
|
||||
name: "View empty memory directory",
|
||||
command: { command: "view" as const, path: "/memories" },
|
||||
},
|
||||
{
|
||||
name: "Create user preferences file",
|
||||
command: {
|
||||
command: "create" as const,
|
||||
path: "/memories/user-preferences.md",
|
||||
file_text:
|
||||
"# User Preferences\n\n- Prefers React with TypeScript\n- Likes clean, readable code\n- Uses functional programming style\n- Prefers ESLint and Prettier for code formatting",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create project notes",
|
||||
command: {
|
||||
command: "create" as const,
|
||||
path: "/memories/project-notes.txt",
|
||||
file_text:
|
||||
"Current Project: React TypeScript App\n\nFeatures to implement:\n1. User authentication\n2. Dashboard with widgets\n3. Data visualization\n4. Export functionality\n\nTech stack:\n- React 18\n- TypeScript\n- Vite\n- Tailwind CSS",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "List directory contents",
|
||||
command: { command: "view" as const, path: "/memories/" },
|
||||
},
|
||||
{
|
||||
name: "Read user preferences",
|
||||
command: {
|
||||
command: "view" as const,
|
||||
path: "/memories/user-preferences.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update preferences (add VS Code)",
|
||||
command: {
|
||||
command: "str_replace" as const,
|
||||
path: "/memories/user-preferences.md",
|
||||
old_str: "- Prefers ESLint and Prettier for code formatting",
|
||||
new_str:
|
||||
"- Prefers ESLint and Prettier for code formatting\n- Uses VS Code as primary editor\n- Likes GitHub Copilot for code completion",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Insert new task in project notes",
|
||||
command: {
|
||||
command: "insert" as const,
|
||||
path: "/memories/project-notes.txt",
|
||||
insert_line: 6,
|
||||
insert_text: "5. Unit testing with Jest and React Testing Library",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Read updated project notes",
|
||||
command: {
|
||||
command: "view" as const,
|
||||
path: "/memories/project-notes.txt",
|
||||
view_range: [4, 8],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
console.log(`\n🔍 ${testCase.name}`)
|
||||
try {
|
||||
const result = await memoryTool.handleCommand(testCase.command)
|
||||
for (const testCase of testCases) {
|
||||
console.log(`\n🔍 ${testCase.name}`)
|
||||
try {
|
||||
const result = await memoryTool.handleCommand(testCase.command)
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Success')
|
||||
if (result.content && result.content.length <= 200) {
|
||||
console.log('📄 Result:', result.content)
|
||||
} else if (result.content) {
|
||||
console.log('📄 Result:', result.content.substring(0, 150) + '... (truncated)')
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Failed')
|
||||
console.log('📄 Error:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('💥 Exception:', error)
|
||||
}
|
||||
if (result.success) {
|
||||
console.log("✅ Success")
|
||||
if (result.content && result.content.length <= 200) {
|
||||
console.log("📄 Result:", result.content)
|
||||
} else if (result.content) {
|
||||
console.log(
|
||||
"📄 Result:",
|
||||
result.content.substring(0, 150) + "... (truncated)",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.log("❌ Failed")
|
||||
console.log("📄 Error:", result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("💥 Exception:", error)
|
||||
}
|
||||
|
||||
// Small delay to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
}
|
||||
// Small delay to avoid rate limiting
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
}
|
||||
}
|
||||
|
||||
// Run the examples
|
||||
async function main() {
|
||||
await testMemoryOperations()
|
||||
console.log('\n' + '=' .repeat(70) + '\n')
|
||||
await chatWithMemoryTool()
|
||||
await testMemoryOperations()
|
||||
console.log("\n" + "=".repeat(70) + "\n")
|
||||
await chatWithMemoryTool()
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main().catch(console.error)
|
||||
}
|
||||
main().catch(console.error)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue