diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx index 795056cf9..be3632740 100644 --- a/apps/web/components/memories-grid.tsx +++ b/apps/web/components/memories-grid.tsx @@ -6,6 +6,7 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import { useInfiniteQuery, useQuery } from "@tanstack/react-query" import { useCallback, memo, useMemo, useState, useRef, useEffect } from "react" import { useQueryState } from "nuqs" +import { AnimatePresence } from "motion/react" import type { z } from "zod" import { Masonry, useInfiniteLoader } from "masonic" import { dmSansClassName } from "@/lib/fonts" @@ -54,11 +55,21 @@ import { CheckIcon, LayoutGrid, Loader, + MoreHorizontal, Trash2Icon, + UserRound, XIcon, } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ui/components/dropdown-menu" import { useProcessingDocuments } from "@/hooks/use-processing-documents" import { TimelineView } from "./timeline-view" +import { SpaceProfilePanel } from "@/components/space-profile-panel" +import { SpaceProfileModal } from "@/components/space-profile-modal" // Document category type type DocumentCategory = @@ -249,6 +260,7 @@ export function MemoriesGrid({ emptyStateProps, }: MemoriesGridProps) { const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false) + const [profileOpen, setProfileOpen] = useState(false) const [localViewMode, setLocalViewMode] = useState<"grid" | "timeline">( () => { if (typeof window === "undefined") return "grid" @@ -259,7 +271,7 @@ export function MemoriesGrid({ }, ) const { user, isSessionPending } = useAuth() - const { effectiveContainerTags } = useProject() + const { effectiveContainerTags, selectedProject } = useProject() const processingStatusMap = useProcessingDocuments() const isMobile = useIsMobile() const [selectedCategories, setSelectedCategories] = useQueryState( @@ -347,6 +359,14 @@ export function MemoriesGrid({ localStorage.setItem("memories-view-mode", mode) }, []) + const handleToggleProfile = useCallback(() => { + if (isMobile) { + setProfileOpen(true) + return + } + setProfileOpen((open) => !open) + }, [isMobile]) + const handleCategoryToggle = useCallback( (category: DocumentCategory) => { setSelectedCategories((prev) => { @@ -541,7 +561,7 @@ export function MemoriesGrid({ documents.every((d) => d.id && selectedDocumentIds.has(d.id)) return ( -
+
{!isEmpty && !isSelectionMode && (
- {onEnterSelectionMode && ( - + + - - - )} + {onEnterSelectionMode && ( + + + Select memories + + )} + + + Space profile + + +
)} @@ -749,64 +804,82 @@ export function MemoriesGrid({ - {error ? ( -
-
- Error loading documents: {error.message} +
+ {error ? ( +
+
+ Error loading documents: {error.message} +
-
- ) : isPending ? ( - - ) : showNovaEmptyState ? ( - - ) : isEmpty ? ( -
-
- No memories found + ) : isPending ? ( + + ) : showNovaEmptyState ? ( + + ) : isEmpty ? ( +
+
+ No memories found +
-
- ) : ( -
- {localViewMode === "timeline" ? ( - - ) : ( - - )} + ) : ( +
+
+ {localViewMode === "timeline" ? ( + + ) : ( + + )} - {isLoadingMore && localViewMode === "grid" && ( -
- + {isLoadingMore && localViewMode === "grid" && ( +
+ +
+ )}
- )} -
- )} + + {profileOpen && !isMobile && ( + setProfileOpen(false)} + /> + )} + +
+ )} +
+
) } diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index f4ab9a529..d43cd6040 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -48,6 +48,7 @@ import { useMemo, useRef, useState } from "react" import { toast } from "sonner" import { useContainerTags } from "@/hooks/use-container-tags" import { PopoverAnchor } from "@ui/components/popover" +import { OrgContext } from "@/components/settings/org-context" function SectionTitle({ children }: { children: React.ReactNode }) { return ( @@ -541,6 +542,8 @@ export default function Account() { + +
diff --git a/apps/web/components/settings/org-context.tsx b/apps/web/components/settings/org-context.tsx new file mode 100644 index 000000000..dac97f7e7 --- /dev/null +++ b/apps/web/components/settings/org-context.tsx @@ -0,0 +1,385 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { LoaderIcon, Settings, X } from "lucide-react" +import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" +import { useOrgSettings, useUpdateOrgSettings } from "@/hooks/use-org-settings" +import { cn } from "@lib/utils" +import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" + +type ContextTemplate = { + id: string + label: string + description: string + prompt: string +} + +const SURFACE_SHADOW = + "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" + +const CONTEXT_TEMPLATES: ContextTemplate[] = [ + { + id: "personal-general", + label: "General Personal Assistant", + description: + "Remember preferences, routines, relationships, plans, and life context.", + prompt: `Supermemory personal assistant. The user saves conversations, notes, and daily context. + +EXTRACT: +- Preferences: "prefers morning meetings", "allergic to peanuts" +- Routines: "works out every Tuesday and Thursday" +- Relationships: "Sarah is their manager", "lives with roommate Jake" +- Plans: "planning a trip to Japan in March" +- Life events: "moved to Austin last month", "started a new job" + +SKIP: +- Generic assistant suggestions the user did not confirm +- Pleasantries and small talk without factual content +- Repeated scheduling details already captured`, + }, + { + id: "personal-productivity", + label: "Productivity Assistant", + description: + "Focus on tasks, decisions, deadlines, workflows, and blockers.", + prompt: `Supermemory productivity tool. The user saves meeting notes, task updates, and project context. + +EXTRACT: +- Action items: "needs to send proposal to client by Friday" +- Decisions: "team decided to use Figma for design handoff" +- Deadlines: "Q3 review due September 15th" +- Workflows: "deploys happen every Wednesday via CI pipeline" +- Blockers: "waiting on legal approval before launch" + +SKIP: +- Meeting filler ("let's circle back", "good point") +- Status updates that repeat previously captured information +- Agenda items with no outcome or decision`, + }, +] + +function SectionTitle({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ) +} + +function SettingsCard({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function PillButton({ + children, + onClick, + disabled, + variant = "default", +}: { + children: React.ReactNode + onClick: () => void + disabled?: boolean + variant?: "default" | "danger" | "primary" +}) { + return ( + + ) +} + +export function OrgContext() { + const { data: settings, isLoading, isError } = useOrgSettings() + const updateSettings = useUpdateOrgSettings() + const [confirmDialog, setConfirmDialog] = useState< + "enable" | "disable" | null + >(null) + const [isManaging, setIsManaging] = useState(false) + const [prompt, setPrompt] = useState("") + + const enabled = settings?.shouldLLMFilter ?? false + const savedPrompt = settings?.filterPrompt ?? "" + const dirty = prompt.trim() !== savedPrompt.trim() + const settingsReady = !isLoading && !isError + + useEffect(() => { + setPrompt(settings?.filterPrompt ?? "") + }, [settings?.filterPrompt]) + + const selectedTemplateId = useMemo(() => { + const normalized = prompt.trim() + return CONTEXT_TEMPLATES.find( + (template) => template.prompt.trim() === normalized, + )?.id + }, [prompt]) + + const handleConfirmToggle = () => { + const newEnabled = confirmDialog === "enable" + updateSettings.mutate( + newEnabled + ? { shouldLLMFilter: true } + : { shouldLLMFilter: false, filterPrompt: null }, + { + onSuccess: () => { + setConfirmDialog(null) + if (!newEnabled) { + setIsManaging(false) + setPrompt("") + } + }, + }, + ) + } + + const handleSave = () => { + updateSettings.mutate( + { + shouldLLMFilter: true, + filterPrompt: prompt.trim() ? prompt.trim() : null, + }, + { + onSuccess: () => { + setIsManaging(false) + }, + }, + ) + } + + const handleCancel = () => { + setPrompt(savedPrompt) + setIsManaging(false) + } + + return ( +
+
+ Organization Context +

+ Guide how Nova processes and remembers your content. +

+
+ +
+
+
+
+

+ {enabled ? "Context is enabled" : "Context is disabled"} +

+

+ {enabled + ? "New content will use your guidance when Nova creates memories." + : "Turn this on to tell Nova what matters most when it learns."} +

+
+
+
+ setConfirmDialog(enabled ? "disable" : "enable")} + disabled={!settingsReady || updateSettings.isPending} + variant={enabled ? "danger" : "primary"} + > + {enabled ? "DISABLE" : "ENABLE"} + + {enabled && ( + setIsManaging(true)} + disabled={updateSettings.isPending} + > + + MANAGE + + )} +
+
+ + {enabled && !isManaging && savedPrompt && ( +
+

+ {savedPrompt} +

+
+ )} + + {enabled && !isManaging && !savedPrompt && ( +

+ No organization context configured.{" "} + +

+ )} + + {enabled && isManaging && ( +
+
+