"use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtomValue } from "jotai"; import { ArrowUp, ChevronDown, ClipboardCopy, Download, Info, Pen } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; import { updateSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { PlateEditor } from "@/components/editor/plate-editor"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Spinner } from "@/components/ui/spinner"; import { baseApiService } from "@/lib/apis/base-api.service"; import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; const MEMORY_HARD_LIMIT = 25_000; const SearchSpaceSchema = z .object({ shared_memory_md: z.string().optional().default(""), }) .passthrough(); interface TeamMemoryManagerProps { searchSpaceId: number; } export function TeamMemoryManager({ searchSpaceId }: TeamMemoryManagerProps) { const queryClient = useQueryClient(); const { data: searchSpace, isLoading: loading } = useQuery({ queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), enabled: !!searchSpaceId, }); const { mutateAsync: updateSearchSpace } = useAtomValue(updateSearchSpaceMutationAtom); const [saving, setSaving] = useState(false); const [editQuery, setEditQuery] = useState(""); const [editing, setEditing] = useState(false); const [showInput, setShowInput] = useState(false); const textareaRef = useRef(null); const inputContainerRef = useRef(null); const memory = searchSpace?.shared_memory_md || ""; useEffect(() => { if (!showInput) return; const handlePointerDownOutside = (event: MouseEvent | TouchEvent) => { const target = event.target; if (!(target instanceof Node)) return; if (inputContainerRef.current?.contains(target)) return; setShowInput(false); }; document.addEventListener("mousedown", handlePointerDownOutside); document.addEventListener("touchstart", handlePointerDownOutside, { passive: true }); return () => { document.removeEventListener("mousedown", handlePointerDownOutside); document.removeEventListener("touchstart", handlePointerDownOutside); }; }, [showInput]); const handleClear = async () => { try { setSaving(true); await updateSearchSpace({ id: searchSpaceId, data: { shared_memory_md: "" }, }); toast.success("Team memory cleared"); } catch { toast.error("Failed to clear team memory"); } finally { setSaving(false); } }; const handleEdit = async () => { const query = editQuery.trim(); if (!query) return; try { setEditing(true); await baseApiService.post( `/api/v1/searchspaces/${searchSpaceId}/memory/edit`, SearchSpaceSchema, { body: { query } } ); setEditQuery(""); setShowInput(false); await queryClient.invalidateQueries({ queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), }); toast.success("Team memory updated"); } catch { toast.error("Failed to edit team memory"); } finally { setEditing(false); } }; const openInput = () => { setShowInput(true); requestAnimationFrame(() => textareaRef.current?.focus()); }; const handleDownload = () => { if (!memory) return; try { const blob = new Blob([memory], { type: "text/markdown;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "team-memory.md"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch { toast.error("Failed to download team memory"); } }; const handleCopyMarkdown = async () => { if (!memory) return; try { await navigator.clipboard.writeText(memory); toast.success("Copied to clipboard"); } catch { toast.error("Failed to copy team memory"); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleEdit(); } }; const displayMemory = memory.replace(/\(\d{4}-\d{2}-\d{2}\)\s*\[(fact|pref|instr)\]\s*/g, ""); const charCount = memory.length; const getCounterColor = () => { if (charCount > MEMORY_HARD_LIMIT) return "text-red-500"; if (charCount > 15_000) return "text-orange-500"; if (charCount > 10_000) return "text-yellow-500"; return "text-muted-foreground"; }; if (loading) { return (
); } if (!memory) { return (

What does SurfSense remember about your team?

Nothing yet. SurfSense picks up on team decisions and conventions as your team chats.

); } return (

SurfSense uses this shared memory to provide team-wide context across all conversations in this search space.

{showInput ? (
setEditQuery(e.target.value)} onKeyDown={handleKeyDown} placeholder="Tell SurfSense what to remember or forget about your team" disabled={editing} className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/70" />
) : ( )}
{charCount.toLocaleString()} / {MEMORY_HARD_LIMIT.toLocaleString()} characters chars {charCount > 15_000 && charCount <= MEMORY_HARD_LIMIT && " - Approaching limit"} {charCount > MEMORY_HARD_LIMIT && " - Exceeds limit"}
Copy as Markdown Download as Markdown
); }