"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, SHARED_TEAM_BRAIN_TAG } from "@lib/constants" import { useHasCompanyBrain } from "@/hooks/use-company-brain" import { ChevronDownIcon, Pencil, 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 { EditSpaceModal } from "./edit-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 { groupAgentSpaces, type AgentSpaceGroup } from "@/lib/agent-space" import NovaOrb from "@/components/nova/nova-orb" import { AutoSpaceIcon } from "@/components/nova/auto-space-icon" import { Logo } from "@ui/assets/Logo" export interface SpaceSelectorProps { selectedProjects: string[] onValueChange: (containerTags: string[]) => void variant?: "default" | "insideOut" triggerClassName?: string showNewSpace?: boolean enableDelete?: boolean compact?: boolean includeAuto?: boolean hideCount?: boolean enableEdit?: boolean } const triggerVariants = { default: "h-10 min-h-10 shrink-0 rounded-full bg-muted px-3 gap-2 " + "hover:bg-white/5 " + "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, enableEdit = false, }: SpaceSelectorProps) { const [showCreateDialog, setShowCreateDialog] = useState(false) const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false) const [showEditDialog, setShowEditDialog] = 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() const hasCompanyBrain = useHasCompanyBrain() const defaultTag = hasCompanyBrain ? SHARED_TEAM_BRAIN_TAG : DEFAULT_PROJECT_ID useEffect(() => { setRecents(readRecents()) }, []) const activeTag = selectedProjects[0] ?? defaultTag const activeTags = selectedProjects.length > 0 ? selectedProjects : [defaultTag] const { data: spaceCountData } = useQuery({ queryKey: ["space-selector-count", activeTags], queryFn: async (): Promise => { const response = await $fetch("@post/documents/documents", { body: { page: 1, limit: 1, sort: "createdAt", order: "desc", containerTags: activeTags, }, 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: activeTags.length > 0 && !activeTags.includes(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 agentGroupByTag = useMemo(() => { const map = new Map>() for (const group of groupAgentSpaces(allProjects, pluginMetaMap)) { for (const tag of group.containerTags) map.set(tag, group) } return map }, [allProjects, pluginMetaMap]) const displayInfo = useMemo<{ name: string emoji: string | null plugin: ReturnType isAuto: boolean isOwnSpace: boolean }>(() => { const containerTag = selectedProjects[0] ?? defaultTag 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 agentGroup = agentGroupByTag.get(containerTag) const isOwnSpace = isOwnConversationSpace({ containerTag }, user?.id) const projectName = agentGroup?.projectName ?? agentGroup?.label ?? pluginMetaMap.get(containerTag)?.projectName const idForLabel = projectName || plugin?.projectId return { name: plugin ? idForLabel ? plugin.pluginId === "agents" ? idForLabel : `${plugin.label} ยท ${idForLabel}` : plugin.label : spaceSelectorDisplayName(found, containerTag, { currentUserId: user?.id, }), emoji: found?.emoji || "๐Ÿ“", plugin, isAuto: false, isOwnSpace, } }, [ allProjects, selectedProjects, pluginMetaMap, agentGroupByTag, includeAuto, user?.id, defaultTag, ]) const canEditCurrent = enableEdit && !displayInfo.isAuto && !displayInfo.plugin && !displayInfo.isOwnSpace 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 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 {canEditCurrent && ( )}
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} /> {canEditCurrent && ( )} { 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

)}
) }