mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-08 22:21:07 +00:00
feat: empty state action for new spaces
This commit is contained in:
parent
a0514e7a44
commit
5b9c1bf8d8
17 changed files with 404 additions and 164 deletions
|
|
@ -221,7 +221,11 @@ export class SupermemoryClient {
|
|||
}
|
||||
|
||||
// Search memories using SDK
|
||||
async search(query: string, limit = 10, threshold?: number): Promise<SearchResult> {
|
||||
async search(
|
||||
query: string,
|
||||
limit = 10,
|
||||
threshold?: number,
|
||||
): Promise<SearchResult> {
|
||||
try {
|
||||
const result = await this.client.search.memories({
|
||||
q: query,
|
||||
|
|
|
|||
|
|
@ -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<typeof DocumentsWithMemoriesResponseSchema>
|
||||
|
|
@ -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("")
|
||||
|
|
@ -295,6 +313,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
|
||||
|
||||
|
|
@ -360,6 +398,16 @@ export default function NewPage() {
|
|||
onShowRelated: handleHighlightsShowRelated,
|
||||
isLoading: isLoadingHighlights,
|
||||
}}
|
||||
emptyStateProps={
|
||||
isNovaContext
|
||||
? {
|
||||
onAddMemory: handleAddMemory,
|
||||
onOpenIntegrations: handleOpenIntegrations,
|
||||
isAllSpaces: isNovaSpaces,
|
||||
spaceName: emptyStateSpaceName,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,9 @@ export default function MigrateMCPPage() {
|
|||
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
|
||||
disabled={migrateMutation.isPending}
|
||||
id="mcpUrl"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMcpUrl(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setMcpUrl(e.target.value)
|
||||
}
|
||||
placeholder="https://mcp.supermemory.ai/userId/sse"
|
||||
type="url"
|
||||
value={mcpUrl}
|
||||
|
|
@ -205,7 +207,9 @@ export default function MigrateMCPPage() {
|
|||
className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
|
||||
disabled={migrateMutation.isPending}
|
||||
id="projectId"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setProjectId(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setProjectId(e.target.value)
|
||||
}
|
||||
placeholder="Project ID (default: 'default')"
|
||||
type="text"
|
||||
value={projectId}
|
||||
|
|
|
|||
|
|
@ -675,104 +675,104 @@ export function ChatSidebar({
|
|||
<HistoryIcon className="size-4 text-[#737373]" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg bg-[#0A0E14] border-[#17181AB2] text-white">
|
||||
<DialogHeader className="pb-4 border-b border-[#17181AB2]">
|
||||
<DialogTitle>Chat History</DialogTitle>
|
||||
<DialogDescription className="text-[#737373]">
|
||||
Project: {selectedProject}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-96">
|
||||
{isLoadingThreads ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<SuperLoader label="Loading..." />
|
||||
</div>
|
||||
) : threads.length === 0 ? (
|
||||
<div className="text-sm text-[#737373] text-center py-8">
|
||||
No conversations yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{threads.map((thread) => {
|
||||
const isActive = thread.id === currentChatId
|
||||
return (
|
||||
<button
|
||||
key={thread.id}
|
||||
type="button"
|
||||
onClick={() => loadThread(thread.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-md px-3 py-2 w-full text-left transition-colors",
|
||||
isActive
|
||||
? "bg-[#267BF1]/10"
|
||||
: "hover:bg-[#17181A]",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium truncate">
|
||||
{thread.title || "Untitled Chat"}
|
||||
</div>
|
||||
<div className="text-xs text-[#737373]">
|
||||
{formatRelativeTime(thread.updatedAt)}
|
||||
</div>
|
||||
<DialogContent className="sm:max-w-lg bg-[#0A0E14] border-[#17181AB2] text-white">
|
||||
<DialogHeader className="pb-4 border-b border-[#17181AB2]">
|
||||
<DialogTitle>Chat History</DialogTitle>
|
||||
<DialogDescription className="text-[#737373]">
|
||||
Project: {selectedProject}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-96">
|
||||
{isLoadingThreads ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<SuperLoader label="Loading..." />
|
||||
</div>
|
||||
) : threads.length === 0 ? (
|
||||
<div className="text-sm text-[#737373] text-center py-8">
|
||||
No conversations yet
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{threads.map((thread) => {
|
||||
const isActive = thread.id === currentChatId
|
||||
return (
|
||||
<button
|
||||
key={thread.id}
|
||||
type="button"
|
||||
onClick={() => loadThread(thread.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-md px-3 py-2 w-full text-left transition-colors",
|
||||
isActive
|
||||
? "bg-[#267BF1]/10"
|
||||
: "hover:bg-[#17181A]",
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium truncate">
|
||||
{thread.title || "Untitled Chat"}
|
||||
</div>
|
||||
{confirmingDeleteId === thread.id ? (
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
deleteThread(thread.id)
|
||||
}}
|
||||
className="bg-red-500 text-white hover:bg-red-600 h-7 w-7"
|
||||
>
|
||||
<Check className="size-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setConfirmingDeleteId(null)
|
||||
}}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<XIcon className="size-3 text-[#737373]" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-[#737373]">
|
||||
{formatRelativeTime(thread.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
{confirmingDeleteId === thread.id ? (
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
deleteThread(thread.id)
|
||||
}}
|
||||
className="bg-red-500 text-white hover:bg-red-600 h-7 w-7"
|
||||
>
|
||||
<Check className="size-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setConfirmingDeleteId(thread.id)
|
||||
setConfirmingDeleteId(null)
|
||||
}}
|
||||
className="h-7 w-7 ml-2"
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<Trash2 className="size-3 text-[#737373]" />
|
||||
<XIcon className="size-3 text-[#737373]" />
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-dashed border-[#73737333] bg-transparent hover:bg-[#17181A]"
|
||||
onClick={() => {
|
||||
handleNewChat()
|
||||
setIsHistoryOpen(false)
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4 mr-1" /> New Conversation
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setConfirmingDeleteId(thread.id)
|
||||
}}
|
||||
className="h-7 w-7 ml-2"
|
||||
>
|
||||
<Trash2 className="size-3 text-[#737373]" />
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-dashed border-[#73737333] bg-transparent hover:bg-[#17181A]"
|
||||
onClick={() => {
|
||||
handleNewChat()
|
||||
setIsHistoryOpen(false)
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4 mr-1" /> New Conversation
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant="headers"
|
||||
className="rounded-full text-base gap-3 h-10! border-[#73737333] bg-[#0D121A] cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -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<IntegrationParamValue, CardId> = {
|
||||
import: "import",
|
||||
chrome: "chrome",
|
||||
connections: "connections",
|
||||
}
|
||||
|
||||
export function IntegrationsView() {
|
||||
const [integration, setIntegration] = useQueryState(
|
||||
"integration",
|
||||
integrationParam,
|
||||
)
|
||||
const [selectedCard, setSelectedCard] = useState<CardId | null>(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":
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
// Document category type
|
||||
type DocumentCategory =
|
||||
|
|
@ -81,11 +82,21 @@ 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
|
||||
quickNoteProps?: QuickNoteProps
|
||||
highlightsProps?: HighlightsProps
|
||||
emptyStateProps?: NovaEmptyStateProps
|
||||
}
|
||||
|
||||
export function MemoriesGrid({
|
||||
|
|
@ -93,9 +104,10 @@ export function MemoriesGrid({
|
|||
onOpenDocument,
|
||||
quickNoteProps,
|
||||
highlightsProps,
|
||||
emptyStateProps,
|
||||
}: MemoriesGridProps) {
|
||||
const { user } = useAuth()
|
||||
const { selectedProjects, effectiveContainerTags } = useProject()
|
||||
const { effectiveContainerTags } = useProject()
|
||||
const isMobile = useIsMobile()
|
||||
const [selectedCategories, setSelectedCategories] = useQueryState(
|
||||
"categories",
|
||||
|
|
@ -284,39 +296,44 @@ export function MemoriesGrid({
|
|||
)
|
||||
}
|
||||
|
||||
const isEmpty = documents.length === 0 && !isPending
|
||||
const showNovaEmptyState = isEmpty && emptyStateProps
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div id="filter-pills" className="flex flex-wrap gap-1.5 mb-3">
|
||||
<Button
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
|
||||
selectedCategories.length === 0 &&
|
||||
"bg-[#00173C] border-[#2261CA33]",
|
||||
)}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
All
|
||||
{facetsData?.total !== undefined && (
|
||||
<span className="ml-1 text-[#737373]">({facetsData.total})</span>
|
||||
)}
|
||||
</Button>
|
||||
{facetsData?.facets.map((facet: DocumentFacet) => (
|
||||
{!isEmpty && (
|
||||
<div id="filter-pills" className="flex flex-wrap gap-1.5 mb-3">
|
||||
<Button
|
||||
key={facet.category}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
|
||||
selectedCategories.includes(facet.category) &&
|
||||
selectedCategories.length === 0 &&
|
||||
"bg-[#00173C] border-[#2261CA33]",
|
||||
)}
|
||||
onClick={() => handleCategoryToggle(facet.category)}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
{facet.label}
|
||||
<span className="ml-1 text-[#737373]">({facet.count})</span>
|
||||
All
|
||||
{facetsData?.total !== undefined && (
|
||||
<span className="ml-1 text-[#737373]">({facetsData.total})</span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{facetsData?.facets.map((facet: DocumentFacet) => (
|
||||
<Button
|
||||
key={facet.category}
|
||||
className={cn(
|
||||
dmSansClassName(),
|
||||
"rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
|
||||
selectedCategories.includes(facet.category) &&
|
||||
"bg-[#00173C] border-[#2261CA33]",
|
||||
)}
|
||||
onClick={() => handleCategoryToggle(facet.category)}
|
||||
>
|
||||
{facet.label}
|
||||
<span className="ml-1 text-[#737373]">({facet.count})</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{error ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<div className="text-center text-muted-foreground">
|
||||
|
|
@ -327,7 +344,14 @@ export function MemoriesGrid({
|
|||
<div className="h-full flex items-center justify-center p-4">
|
||||
<SuperLoader />
|
||||
</div>
|
||||
) : documents.length === 0 && !isPending ? (
|
||||
) : showNovaEmptyState ? (
|
||||
<NovaEmptyState
|
||||
onAddMemory={emptyStateProps.onAddMemory}
|
||||
onOpenIntegrations={emptyStateProps.onOpenIntegrations}
|
||||
isAllSpaces={emptyStateProps.isAllSpaces}
|
||||
spaceName={emptyStateProps.spaceName}
|
||||
/>
|
||||
) : isEmpty ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<div className="text-center text-muted-foreground">
|
||||
No memories found
|
||||
|
|
|
|||
|
|
@ -445,9 +445,7 @@ export const MemoryGraph = ({
|
|||
variant={variant}
|
||||
/>
|
||||
|
||||
{!isLoading &&
|
||||
!nodes.some((n) => n.type === "document") &&
|
||||
children}
|
||||
{!isLoading && !nodes.some((n) => n.type === "document") && children}
|
||||
|
||||
<div
|
||||
className="w-full h-full relative overflow-hidden touch-none select-none"
|
||||
|
|
|
|||
146
apps/web/components/nova/nova-empty-state.tsx
Normal file
146
apps/web/components/nova/nova-empty-state.tsx
Normal file
|
|
@ -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 (
|
||||
<div
|
||||
id="nova-empty-state"
|
||||
className="min-h-[calc(100dvh-12rem)] flex items-center justify-center p-6 md:p-8 opacity-50 hover:opacity-100 transition-opacity duration-300"
|
||||
>
|
||||
<div className="max-w-xl w-full flex flex-col items-center text-center">
|
||||
<NovaOrb size={80} className="blur-[2px]! mb-4" />
|
||||
<h2
|
||||
className={cn(
|
||||
"text-white text-xl md:text-2xl font-medium mb-2",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className={cn("text-[#8B8B8B] text-sm mb-6", dmSansClassName())}>
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 w-full mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddMemory("link")}
|
||||
className={cardClass}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="size-5 text-[#737373]" />
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-white text-sm",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Save a link
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[#4BA0FA] text-xs font-medium flex items-center gap-1">
|
||||
Add now <ArrowRight className="size-3" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddMemory("note")}
|
||||
className={cardClass}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="size-5 text-[#737373]" />
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-white text-sm",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Write a note
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[#4BA0FA] text-xs font-medium flex items-center gap-1">
|
||||
Add now <ArrowRight className="size-3" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInstallChrome}
|
||||
className={cardClass}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChromeIcon className="size-5" />
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-white text-sm",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
Chrome Extension
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[#4BA0FA] text-xs font-medium flex items-center gap-1">
|
||||
Install <ArrowRight className="size-3" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onOpenIntegrations()}
|
||||
className={cn(
|
||||
"text-[#737373] hover:text-white hover:bg-transparent",
|
||||
"flex items-center gap-1.5",
|
||||
dmSansClassName(),
|
||||
)}
|
||||
>
|
||||
<Zap className="size-4" />
|
||||
See more integrations
|
||||
<ArrowRight className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ const SEARCH_MEMORY_SHORTCUT_URL =
|
|||
const ADD_MEMORY_SHORTCUT_URL =
|
||||
"https://www.icloud.com/shortcuts/0fd3e855be444845b457f94c78c2c8d9"
|
||||
const RAYCAST_EXTENSION_URL = "https://www.raycast.com/supermemory/supermemory"
|
||||
const CHROME_EXTENSION_URL = "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc"
|
||||
const CHROME_EXTENSION_URL =
|
||||
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc"
|
||||
|
||||
export {
|
||||
BIG_DIMENSIONS_NEW,
|
||||
|
|
|
|||
|
|
@ -740,10 +740,9 @@ export const MemoryGraph = ({
|
|||
)}
|
||||
|
||||
{/* Show welcome screen when no memories exist */}
|
||||
{!isLoading &&
|
||||
(!data || !nodes.some((n) => n.type === "document")) && (
|
||||
<>{children}</>
|
||||
)}
|
||||
{!isLoading && (!data || !nodes.some((n) => n.type === "document")) && (
|
||||
<>{children}</>
|
||||
)}
|
||||
|
||||
{/* Graph container */}
|
||||
<div className={styles.graphContainer} ref={containerRef}>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ export interface ProfileResult {
|
|||
export interface DocumentListResult {
|
||||
success: boolean
|
||||
documents?: Awaited<ReturnType<Supermemory["documents"]["list"]>>["documents"]
|
||||
pagination?: Awaited<ReturnType<Supermemory["documents"]["list"]>>["pagination"]
|
||||
pagination?: Awaited<
|
||||
ReturnType<Supermemory["documents"]["list"]>
|
||||
>["pagination"]
|
||||
error?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
|
@ -13,14 +13,14 @@
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,10 +409,9 @@ export const MemoryGraph = ({
|
|||
</AnimatePresence>
|
||||
|
||||
{/* Show welcome screen when no memories exist */}
|
||||
{!isLoading &&
|
||||
(!data || !nodes.some((n) => n.type === "document")) && (
|
||||
<>{children}</>
|
||||
)}
|
||||
{!isLoading && (!data || !nodes.some((n) => n.type === "document")) && (
|
||||
<>{children}</>
|
||||
)}
|
||||
|
||||
{/* Graph container */}
|
||||
<div
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue