diff --git a/apps/mcp/src/client.ts b/apps/mcp/src/client.ts index df185f3f..2924f6fb 100644 --- a/apps/mcp/src/client.ts +++ b/apps/mcp/src/client.ts @@ -221,7 +221,11 @@ export class SupermemoryClient { } // Search memories using SDK - async search(query: string, limit = 10, threshold?: number): Promise { + async search( + query: string, + limit = 10, + threshold?: number, + ): Promise { try { const result = await this.client.search.memories({ q: query, diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 634db807..fb82bcb9 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -19,6 +19,8 @@ import { AnimatePresence } from "motion/react" import { useIsMobile } from "@hooks/use-mobile" import { useAuth } from "@lib/auth-context" import { useProject } from "@/stores" +import { useContainerTags } from "@/hooks/use-container-tags" +import { DEFAULT_PROJECT_ID } from "@lib/constants" import { useQuickNoteDraftReset, useQuickNoteDraft, @@ -38,6 +40,7 @@ import { docParam, fullscreenParam, chatParam, + integrationParam, } from "@/lib/search-params" type DocumentsResponse = z.infer @@ -63,7 +66,21 @@ function ViewErrorFallback() { export default function NewPage() { const isMobile = useIsMobile() const { user, session } = useAuth() - const { selectedProject, isNovaSpaces, novaContainerTags } = useProject() + const { selectedProject, isNovaSpaces, novaContainerTags, selectedProjects } = + useProject() + const selectedProjectTag = selectedProjects[0] + const isNovaContext = + isNovaSpaces || + (selectedProjectTag !== undefined && + novaContainerTags.includes(selectedProjectTag)) + const { allProjects } = useContainerTags() + const emptyStateSpaceName = + !isNovaSpaces && selectedProjectTag + ? selectedProjectTag === DEFAULT_PROJECT_ID + ? "My Space" + : (allProjects.find((p) => p.containerTag === selectedProjectTag) + ?.name ?? selectedProjectTag) + : undefined const { viewMode, setViewMode } = useViewMode() const queryClient = useQueryClient() @@ -93,6 +110,7 @@ export default function NewPage() { fullscreenParam, ) const [isChatOpen, setIsChatOpen] = useQueryState("chat", chatParam) + const [, setIntegration] = useQueryState("integration", integrationParam) // Ephemeral local state (not worth URL-encoding) const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") @@ -348,6 +366,26 @@ export default function NewPage() { [setSearchPrefill, setIsSearchOpen], ) + const handleOpenIntegrations = useCallback( + (integration?: "import" | "chrome" | "connections") => { + setViewMode("integrations") + if (integration) { + setIntegration(integration) + } else { + setIntegration(null) + } + }, + [setViewMode, setIntegration], + ) + + const handleAddMemory = useCallback( + (tab: "note" | "link") => { + analytics.addDocumentModalOpened() + setAddDoc(tab) + }, + [setAddDoc], + ) + const chatOpen = isChatOpen !== null ? isChatOpen : !isMobile const isGraphMode = viewMode === "graph" && !isMobile @@ -421,6 +459,16 @@ export default function NewPage() { onShowRelated: handleHighlightsShowRelated, isLoading: isLoadingHighlights, }} + emptyStateProps={ + isNovaContext + ? { + onAddMemory: handleAddMemory, + onOpenIntegrations: handleOpenIntegrations, + isAllSpaces: isNovaSpaces, + spaceName: emptyStateSpaceName, + } + : undefined + } /> )} diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 842711f8..ae73b782 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -1,6 +1,7 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" +import { useQueryState } from "nuqs" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" import { Button } from "@ui/components/button" @@ -18,6 +19,10 @@ import { } from "@/components/integration-icons" import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" import { ArrowLeft, Sun } from "lucide-react" +import { + integrationParam, + type IntegrationParamValue, +} from "@/lib/search-params" import Image from "next/image" type CardId = @@ -140,10 +145,29 @@ function DetailWrapper({ ) } +const INTEGRATION_TO_CARD: Record = { + import: "import", + chrome: "chrome", + connections: "connections", +} + export function IntegrationsView() { + const [integration, setIntegration] = useQueryState( + "integration", + integrationParam, + ) const [selectedCard, setSelectedCard] = useState(null) - const handleBack = () => setSelectedCard(null) + useEffect(() => { + if (integration && INTEGRATION_TO_CARD[integration]) { + setSelectedCard(INTEGRATION_TO_CARD[integration]) + } + }, [integration]) + + const handleBack = () => { + setSelectedCard(null) + setIntegration(null) + } switch (selectedCard) { case "mcp": diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index e7f54f8b..cc7e8453 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -31,6 +31,7 @@ import { HighlightsCard, type HighlightItem } from "./highlights-card" import { GraphCard } from "./memory-graph" import { Button } from "@ui/components/button" import { categoriesParam } from "@/lib/search-params" +import { NovaEmptyState } from "@/components/nova/nova-empty-state" import { AlertDialog, AlertDialogAction, @@ -92,6 +93,15 @@ interface HighlightsProps { isLoading: boolean } +interface NovaEmptyStateProps { + onAddMemory: (tab: "note" | "link") => void + onOpenIntegrations: ( + integration?: "import" | "chrome" | "connections", + ) => void + isAllSpaces: boolean + spaceName?: string +} + interface MemoriesGridProps { isChatOpen: boolean onOpenDocument: (document: DocumentWithMemories) => void @@ -105,6 +115,7 @@ interface MemoriesGridProps { isBulkDeleting?: boolean quickNoteProps?: QuickNoteProps highlightsProps?: HighlightsProps + emptyStateProps?: NovaEmptyStateProps } export function MemoriesGrid({ @@ -120,6 +131,7 @@ export function MemoriesGrid({ isBulkDeleting = false, quickNoteProps, highlightsProps, + emptyStateProps, }: MemoriesGridProps) { const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false) const { user } = useAuth() @@ -340,99 +352,107 @@ export function MemoriesGrid({ ) } + const isEmpty = documents.length === 0 && !isPending + const showNovaEmptyState = isEmpty && emptyStateProps + return (
-
-
- - {facetsData?.facets.map((facet: DocumentFacet) => ( + {!isEmpty && ( +
+
- ))} -
- -
- {isSelectionMode && ( - <> + {facetsData?.facets.map((facet: DocumentFacet) => ( + + ))} +
+
+ {isSelectionMode && ( + <> + + {selectedDocumentIds.size > 0 ? ( + <> + + + + ) : ( +

+ Select one or more documents +

+ )} + + )} + {!isSelectionMode && onEnterSelectionMode && ( - {selectedDocumentIds.size > 0 ? ( - <> - - - - ) : ( -

- Select one or more documents -

- )} - - )} - {!isSelectionMode && onEnterSelectionMode && ( - - )} + )} +
-
+ )}
- ) : documents.length === 0 && !isPending ? ( + ) : showNovaEmptyState ? ( + + ) : isEmpty ? (
No memories found diff --git a/apps/web/components/nova/nova-empty-state.tsx b/apps/web/components/nova/nova-empty-state.tsx new file mode 100644 index 00000000..8156512f --- /dev/null +++ b/apps/web/components/nova/nova-empty-state.tsx @@ -0,0 +1,146 @@ +"use client" + +import { CHROME_EXTENSION_URL } from "@repo/lib/constants" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/lib/fonts" +import { Button } from "@ui/components/button" +import NovaOrb from "./nova-orb" +import { ChromeIcon } from "@/components/integration-icons" +import { ArrowRight, Link2, FileText, Zap } from "lucide-react" + +interface NovaEmptyStateProps { + onAddMemory: (tab: "note" | "link") => void + onOpenIntegrations: ( + integration?: "import" | "chrome" | "connections", + ) => void + isAllSpaces: boolean + spaceName?: string +} + +const cardClass = cn( + "bg-[#14161A] rounded-xl p-4 border border-[rgba(82,89,102,0.2)]", + "hover:border-[#3374FF]/50 hover:bg-[#1B1F24]", + "transition-colors cursor-pointer text-left flex flex-col gap-2", +) + +export function NovaEmptyState({ + onAddMemory, + onOpenIntegrations, + isAllSpaces, + spaceName, +}: NovaEmptyStateProps) { + const handleInstallChrome = () => { + window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer") + } + + const title = isAllSpaces + ? "Help Nova get to know you" + : "This space is empty" + const subtitle = isAllSpaces + ? "Add your first memory to get started." + : spaceName + ? `Add memories to ${spaceName} to get started.` + : "Add memories to this space to get started." + + return ( +
+
+ +

+ {title} +

+

+ {subtitle} +

+ +
+ + + + + +
+ + +
+
+ ) +} diff --git a/apps/web/globals.css b/apps/web/globals.css index 647a85de..82dccd8b 100644 --- a/apps/web/globals.css +++ b/apps/web/globals.css @@ -109,7 +109,8 @@ } /* Override prose paragraph margins for text editor */ -.text-editor-prose.prose :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { +.text-editor-prose.prose +:where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) { margin-top: 0; margin-bottom: 0; } diff --git a/apps/web/lib/search-params.ts b/apps/web/lib/search-params.ts index 93594648..67d74ccb 100644 --- a/apps/web/lib/search-params.ts +++ b/apps/web/lib/search-params.ts @@ -24,6 +24,9 @@ export const feedbackParam = parseAsBoolean.withDefault(false) // View & filter states const viewLiterals = ["graph", "list", "integrations"] as const +const integrationLiterals = ["import", "chrome", "connections"] as const +export type IntegrationParamValue = (typeof integrationLiterals)[number] +export const integrationParam = parseAsStringLiteral(integrationLiterals) export type ViewParamValue = (typeof viewLiterals)[number] export const viewParam = parseAsStringLiteral(viewLiterals).withDefault("list") export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault( diff --git a/biome.json b/biome.json index ea4876ba..c4e233d6 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,10 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", + "css": { + "parser": { + "tailwindDirectives": true + } + }, "assist": { "actions": { "source": { diff --git a/packages/tools/src/ai-sdk.ts b/packages/tools/src/ai-sdk.ts index 6cd2d612..77f3ad03 100644 --- a/packages/tools/src/ai-sdk.ts +++ b/packages/tools/src/ai-sdk.ts @@ -135,14 +135,8 @@ export const getProfileTool = ( inputSchema: z.object({ containerTag: strict ? z.string().describe(PARAMETER_DESCRIPTIONS.containerTag) - : z - .string() - .optional() - .describe(PARAMETER_DESCRIPTIONS.containerTag), - query: z - .string() - .optional() - .describe(PARAMETER_DESCRIPTIONS.query), + : z.string().optional().describe(PARAMETER_DESCRIPTIONS.containerTag), + query: z.string().optional().describe(PARAMETER_DESCRIPTIONS.query), }), execute: async ({ containerTag, query }) => { try { @@ -197,14 +191,8 @@ export const documentListTool = ( .optional() .default(DEFAULT_VALUES.limit) .describe(PARAMETER_DESCRIPTIONS.limit), - offset: z - .number() - .optional() - .describe(PARAMETER_DESCRIPTIONS.offset), - status: z - .string() - .optional() - .describe(PARAMETER_DESCRIPTIONS.status), + offset: z.number().optional().describe(PARAMETER_DESCRIPTIONS.offset), + status: z.string().optional().describe(PARAMETER_DESCRIPTIONS.status), }), execute: async ({ containerTag, limit, offset, status }) => { try { @@ -329,10 +317,7 @@ export const memoryForgetTool = ( .string() .optional() .describe(PARAMETER_DESCRIPTIONS.containerTag), - memoryId: z - .string() - .optional() - .describe(PARAMETER_DESCRIPTIONS.memoryId), + memoryId: z.string().optional().describe(PARAMETER_DESCRIPTIONS.memoryId), memoryContent: z .string() .optional() diff --git a/packages/tools/src/openai/tools.ts b/packages/tools/src/openai/tools.ts index aa1f3114..da7d6642 100644 --- a/packages/tools/src/openai/tools.ts +++ b/packages/tools/src/openai/tools.ts @@ -37,7 +37,9 @@ export interface ProfileResult { export interface DocumentListResult { success: boolean documents?: Awaited>["documents"] - pagination?: Awaited>["pagination"] + pagination?: Awaited< + ReturnType + >["pagination"] error?: string } diff --git a/packages/tools/src/tools-shared.ts b/packages/tools/src/tools-shared.ts index 1ea20db5..ac5dfcd3 100644 --- a/packages/tools/src/tools-shared.ts +++ b/packages/tools/src/tools-shared.ts @@ -31,13 +31,15 @@ export const PARAMETER_DESCRIPTIONS = { containerTag: "Tag to filter/scope the operation (e.g., user ID, project ID)", query: "Optional search query to include relevant search results", offset: "Number of items to skip for pagination (default: 0)", - status: "Filter documents by processing status (e.g., 'completed', 'processing', 'failed')", + status: + "Filter documents by processing status (e.g., 'completed', 'processing', 'failed')", documentId: "The unique identifier of the document to operate on", content: "The content to add - can be text, URL, or other supported formats", title: "Optional title for the document", description: "Optional description for the document", memoryId: "The unique identifier of the memory entry", - memoryContent: "Exact content match of the memory entry to operate on (alternative to ID)", + memoryContent: + "Exact content match of the memory entry to operate on (alternative to ID)", reason: "Optional reason for forgetting this memory", } as const diff --git a/packages/tools/test/chatapp/app/globals.css b/packages/tools/test/chatapp/app/globals.css index a2dc41ec..0673ff02 100644 --- a/packages/tools/test/chatapp/app/globals.css +++ b/packages/tools/test/chatapp/app/globals.css @@ -1,26 +1,26 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #ffffff; + --foreground: #171717; } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); } @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + :root { + --background: #0a0a0a; + --foreground: #ededed; + } } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; } diff --git a/packages/ui/memory-graph/memory-graph.tsx b/packages/ui/memory-graph/memory-graph.tsx index 97b0a604..b0d0ae77 100644 --- a/packages/ui/memory-graph/memory-graph.tsx +++ b/packages/ui/memory-graph/memory-graph.tsx @@ -410,9 +410,8 @@ export const MemoryGraph = ({ {/* Show welcome screen when no memories exist */} {!isLoading && - (!data || !nodes.some((n) => n.type === "document")) && ( - <>{children} - )} + (!data || !nodes.some((n) => n.type === "document")) && + children} {/* Graph container */}