"use client" import { useState, useMemo, useEffect, useCallback } from "react" import Image from "next/image" import { useQuery } from "@tanstack/react-query" import { cn } from "@lib/utils" import { $fetch } from "@lib/api" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { DEFAULT_PROJECT_ID } from "@lib/constants" import { XIcon, Loader2 } from "lucide-react" import type { ContainerTagListType } from "@lib/types" import { AddSpaceModal } from "./add-space-modal" import { SelectSpacesModal } from "./select-spaces-modal" import { useProjectMutations } from "@/hooks/use-project-mutations" import { useContainerTags } from "@/hooks/use-container-tags" import { motion } from "motion/react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { Dialog, DialogContent, DialogTitle, DialogDescription, } from "@repo/ui/components/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@repo/ui/components/select" import { Button } from "@repo/ui/components/button" import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip" import { analytics } from "@/lib/analytics" import { compareSpacesUserFirst, spaceSelectorDisplayName, } from "@/lib/ingest-auto-space" import { detectPluginSpace, pluginInitial } from "@/lib/plugin-space" import { usePluginSpaceMeta } from "@/hooks/use-plugin-space-meta" export interface SpaceSelectorProps { selectedProjects: string[] onValueChange: (containerTags: string[]) => void variant?: "default" | "insideOut" triggerClassName?: string showNewSpace?: boolean enableDelete?: boolean compact?: boolean } const triggerVariants = { default: "h-10 min-h-10 shrink-0 rounded-full border border-[#161F2C] bg-muted px-3 gap-2 " + "hover:bg-white/5 hover:border-[#2261CA33] " + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2261CA33]/35", insideOut: "h-10 min-h-10 gap-2 px-3 rounded-full bg-[#0D121A] shadow-inside-out hover:bg-[#121820]", } const RECENTS_KEY = "nova:space-selector:recents" const RECENTS_MAX = 10 function readRecents(): string[] { if (typeof window === "undefined") return [] try { const raw = window.localStorage.getItem(RECENTS_KEY) if (!raw) return [] const parsed = JSON.parse(raw) return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [] } catch { return [] } } function writeRecents(tags: string[]) { if (typeof window === "undefined") return try { window.localStorage.setItem(RECENTS_KEY, JSON.stringify(tags)) } catch { // ignore } } function formatCount(n: number): string { if (n < 1000) return String(n) if (n < 10_000) return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` if (n < 1_000_000) return `${Math.floor(n / 1000)}k` return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}m` } export function SpaceSelector({ selectedProjects, onValueChange, variant = "default", triggerClassName, showNewSpace = true, enableDelete = false, compact = false, }: SpaceSelectorProps) { const [showCreateDialog, setShowCreateDialog] = useState(false) const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false) const [recents, setRecents] = useState([]) const [deleteDialog, setDeleteDialog] = useState<{ open: boolean project: { id: string; name: string; containerTag: string } | null action: "move" | "delete" targetProjectId: string }>({ open: false, project: null, action: "move", targetProjectId: "", }) const { deleteProjectMutation } = useProjectMutations() const { allProjects, isLoading } = useContainerTags() useEffect(() => { setRecents(readRecents()) }, []) const activeTag = selectedProjects[0] ?? DEFAULT_PROJECT_ID const { data: spaceCountData } = useQuery({ queryKey: ["space-selector-count", activeTag], queryFn: async (): Promise => { const response = await $fetch("@post/documents/documents", { body: { page: 1, limit: 1, sort: "createdAt", order: "desc", containerTags: [activeTag], }, disableValidation: true, }) if (response.error) return 0 const data = response.data as { pagination?: { totalItems?: number } } | null return data?.pagination?.totalItems ?? 0 }, staleTime: 30 * 1000, enabled: !!activeTag, }) const pluginTags = useMemo( () => allProjects .filter( (p: ContainerTagListType) => !!detectPluginSpace(p.containerTag), ) .map((p: ContainerTagListType) => p.containerTag), [allProjects], ) const pluginMetaMap = usePluginSpaceMeta(pluginTags) const displayInfo = useMemo<{ name: string emoji: string | null plugin: ReturnType }>(() => { const containerTag = selectedProjects[0] ?? "" if (!containerTag || containerTag === DEFAULT_PROJECT_ID) { return { name: "My Space", emoji: "๐Ÿ“", plugin: null } } const found = allProjects.find( (p: ContainerTagListType) => p.containerTag === containerTag, ) const plugin = detectPluginSpace(containerTag) const projectName = pluginMetaMap.get(containerTag)?.projectName const idForLabel = projectName || plugin?.projectId return { name: plugin ? idForLabel ? `${plugin.label} ยท ${idForLabel}` : plugin.label : spaceSelectorDisplayName(found, containerTag), emoji: found?.emoji || "๐Ÿ“", plugin, } }, [allProjects, selectedProjects, pluginMetaMap]) const pushRecent = useCallback((tag: string) => { setRecents((prev) => { const next = [tag, ...prev.filter((t) => t !== tag)].slice(0, RECENTS_MAX) writeRecents(next) return next }) }, []) const handleSelectSpacesApply = useCallback( (selected: string[]) => { const next = selected.slice(0, 1) if (next[0]) { analytics.spaceSwitched({ space_id: next[0] }) pushRecent(next[0]) } onValueChange(next) setShowSelectSpacesModal(false) }, [onValueChange, pushRecent], ) const handleNewSpace = useCallback(() => { setShowSelectSpacesModal(false) setShowCreateDialog(true) }, []) const handleDeleteRequest = useCallback( (project: { id: string; name: string; containerTag: string }) => { setShowSelectSpacesModal(false) setDeleteDialog({ open: true, project, action: "move", targetProjectId: "", }) }, [], ) const handleDeleteConfirm = () => { if (!deleteDialog.project) return deleteProjectMutation.mutate( { projectId: deleteDialog.project.id, action: deleteDialog.action, targetProjectId: deleteDialog.action === "move" ? deleteDialog.targetProjectId : undefined, }, { onSuccess: () => { setDeleteDialog({ open: false, project: null, action: "move", targetProjectId: "", }) }, }, ) } const handleDeleteCancel = () => { setDeleteDialog({ open: false, project: null, action: "move", targetProjectId: "", }) } const availableTargetProjects = useMemo(() => { const filtered = allProjects.filter( (p: ContainerTagListType) => p.id !== deleteDialog.project?.id && p.containerTag !== deleteDialog.project?.containerTag, ) const defaultProject = allProjects.find( (p: ContainerTagListType) => p.containerTag === DEFAULT_PROJECT_ID, ) const isDefaultProjectBeingDeleted = deleteDialog.project?.containerTag === DEFAULT_PROJECT_ID if (defaultProject && !isDefaultProjectBeingDeleted) { const defaultProjectIncluded = filtered.some( (p: ContainerTagListType) => p.containerTag === DEFAULT_PROJECT_ID, ) if (!defaultProjectIncluded) return [defaultProject, ...filtered] } return filtered.sort(compareSpacesUserFirst) }, [allProjects, deleteDialog.project]) return ( <> Switch space setShowCreateDialog(false)} onCreated={(containerTag) => { pushRecent(containerTag) onValueChange([containerTag]) }} /> setShowSelectSpacesModal(false)} selectedProjects={selectedProjects} onApply={handleSelectSpacesApply} projects={allProjects} recents={recents} showNewSpace={showNewSpace} onNewSpace={handleNewSpace} enableDelete={enableDelete} onDeleteRequest={handleDeleteRequest} /> { if (!open) { setDeleteDialog({ open: false, project: null, action: "move", targetProjectId: "", }) } }} >
Delete space What would you like to do with the documents and memories in{" "} "{deleteDialog.project?.name}" ?
Close
{deleteDialog.action === "move" && ( )} {deleteDialog.action === "delete" && ( All documents and memories will be permanently deleted. )}
) }