"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 { ChevronDownIcon, XIcon, Loader2, Trash2 } from "lucide-react" import type { ContainerTagListType } from "@lib/types" import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import { AddSpaceModal } from "./add-space-modal" import { SelectSpacesModal } from "./select-spaces-modal" import { SpaceGlyph } from "./space-glyph" 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 { useAuth } from "@lib/auth-context" import { analytics } from "@/lib/analytics" import { compareSpacesUserFirst, isOwnConversationSpace, spaceSelectorDisplayName, } from "@/lib/ingest-auto-space" import { detectPluginSpace, pluginInitial } from "@/lib/plugin-space" import { usePluginSpaceMeta } from "@/hooks/use-plugin-space-meta" import NovaOrb from "@/components/nova/nova-orb" import { AutoSpaceIcon } from "@/components/nova/auto-space-icon" export interface SpaceSelectorProps { selectedProjects: string[] onValueChange: (containerTags: string[]) => void variant?: "default" | "insideOut" triggerClassName?: string showNewSpace?: boolean enableDelete?: boolean compact?: boolean includeAuto?: boolean hideCount?: 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 type DeleteProjectTarget = { id: string name: string containerTag: string } 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, includeAuto = false, hideCount = false, }: SpaceSelectorProps) { const [showCreateDialog, setShowCreateDialog] = useState(false) const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false) const [recents, setRecents] = useState([]) const [deleteDialog, setDeleteDialog] = useState<{ open: boolean project: DeleteProjectTarget | null action: "move" | "delete" targetProjectId: string }>({ open: false, project: null, action: "move", targetProjectId: "", }) const [bulkDeleteDialog, setBulkDeleteDialog] = useState<{ open: boolean projects: DeleteProjectTarget[] confirmation: string }>({ open: false, projects: [], confirmation: "", }) const { deleteProjectMutation, deleteProjectsMutation } = useProjectMutations() const { allProjects, isLoading } = useContainerTags() const { user } = useAuth() 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 && activeTag !== AUTO_CHAT_SPACE_ID, }) 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 isAuto: boolean isOwnSpace: boolean }>(() => { const containerTag = selectedProjects[0] ?? "" if (includeAuto && containerTag === AUTO_CHAT_SPACE_ID) { return { name: "Auto", emoji: null, plugin: null, isAuto: true, isOwnSpace: false, } } if (!containerTag || containerTag === DEFAULT_PROJECT_ID) { return { name: "My Space", emoji: "📁", plugin: null, isAuto: false, isOwnSpace: false, } } const found = allProjects.find( (p: ContainerTagListType) => p.containerTag === containerTag, ) const plugin = detectPluginSpace(containerTag) const isOwnSpace = isOwnConversationSpace({ containerTag }, user?.id) const projectName = pluginMetaMap.get(containerTag)?.projectName const idForLabel = projectName || plugin?.projectId return { name: plugin ? idForLabel ? `${plugin.label} · ${idForLabel}` : plugin.label : spaceSelectorDisplayName(found, containerTag, { currentUserId: user?.id, }), emoji: found?.emoji || "📁", plugin, isAuto: false, isOwnSpace, } }, [allProjects, selectedProjects, pluginMetaMap, includeAuto, user?.id]) 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) const selectedTag = next[0] setShowSelectSpacesModal(false) onValueChange(next) if (selectedTag && selectedTag !== AUTO_CHAT_SPACE_ID) { queueMicrotask(() => { analytics.spaceSwitched({ space_id: selectedTag }) pushRecent(selectedTag) }) } }, [onValueChange, pushRecent], ) const handleNewSpace = useCallback(() => { setShowSelectSpacesModal(false) setShowCreateDialog(true) }, []) const handleDeleteRequest = useCallback((project: DeleteProjectTarget) => { setShowSelectSpacesModal(false) setDeleteDialog({ open: true, project, action: "move", targetProjectId: "", }) }, []) const handleBulkDeleteRequest = useCallback( (projects: DeleteProjectTarget[]) => { if (projects.length === 0) return setShowSelectSpacesModal(false) setBulkDeleteDialog({ open: true, projects, confirmation: "", }) }, [], ) const handleDeleteConfirm = () => { if (!deleteDialog.project) return deleteProjectMutation.mutate( { projectId: deleteDialog.project.id, containerTag: deleteDialog.project.containerTag, 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 handleBulkDeleteCancel = () => { setBulkDeleteDialog({ open: false, projects: [], confirmation: "", }) } const handleBulkDeleteConfirm = () => { if ( bulkDeleteDialog.confirmation !== "DELETE" || bulkDeleteDialog.projects.length === 0 ) { return } deleteProjectsMutation.mutate( { projects: bulkDeleteDialog.projects, }, { onSettled: () => { setBulkDeleteDialog({ open: false, projects: [], confirmation: "", }) }, }, ) } 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} includeAuto={includeAuto} onNewSpace={handleNewSpace} enableDelete={enableDelete} onDeleteRequest={handleDeleteRequest} onBulkDeleteRequest={handleBulkDeleteRequest} /> { 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. )}
{ if (!open) handleBulkDeleteCancel() }} >
Delete {bulkDeleteDialog.projects.length}{" "} {bulkDeleteDialog.projects.length === 1 ? "space" : "spaces"}? This permanently deletes the selected container tags and every document and memory inside them. This cannot be undone.
Close
{bulkDeleteDialog.projects.slice(0, 8).map((project) => (
{project.name}
))} {bulkDeleteDialog.projects.length > 8 && (

+{bulkDeleteDialog.projects.length - 8} more

)}
) }