core/apps/webapp/app/components/spaces/space-dropdown.tsx
Harshith Mullapudi bcc0560cf0
Feat: Space (#93)
* Feat: change space assignment from statement to episode

* feat: add default spaces and improve integration, space tools discovery in MCP

* feat: change spaces to episode based

* Feat: take multiple spaceIds while ingesting

* Feat: modify mcp tool descriptions, add spaceId in mcp url

* feat: add copy

* bump: new version 0.1.24

---------

Co-authored-by: Manoj <saimanoj58@gmail.com>
2025-10-09 12:38:42 +05:30

168 lines
4.4 KiB
TypeScript

import { useState, useEffect } from "react";
import { Check, Plus, X } from "lucide-react";
import { Button } from "~/components/ui/button";
import {
Popover,
PopoverContent,
PopoverPortal,
PopoverTrigger,
} from "~/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import { Badge } from "~/components/ui/badge";
import { cn } from "~/lib/utils";
import { useFetcher } from "@remix-run/react";
import { Project } from "../icons/project";
interface Space {
id: string;
name: string;
description?: string;
}
interface SpaceDropdownProps {
episodeIds: string[];
selectedSpaceIds?: string[];
onSpaceChange?: (spaceIds: string[]) => void;
className?: string;
}
export function SpaceDropdown({
episodeIds,
selectedSpaceIds = [],
onSpaceChange,
className,
}: SpaceDropdownProps) {
const [open, setOpen] = useState(false);
const [selectedSpaces, setSelectedSpaces] =
useState<string[]>(selectedSpaceIds);
const [spaces, setSpaces] = useState<Space[]>([]);
const spacesFetcher = useFetcher<{ spaces: Space[] }>();
const assignFetcher = useFetcher();
// Fetch all spaces
useEffect(() => {
spacesFetcher.load("/api/v1/spaces");
}, []);
// Update spaces when data is fetched
useEffect(() => {
if (spacesFetcher.data?.spaces) {
setSpaces(spacesFetcher.data.spaces);
}
}, [spacesFetcher.data]);
const handleSpaceToggle = (spaceId: string) => {
const newSelectedSpaces = selectedSpaces.includes(spaceId)
? selectedSpaces.filter((id) => id !== spaceId)
: [...selectedSpaces, spaceId];
setSelectedSpaces(newSelectedSpaces);
if (episodeIds) {
assignFetcher.submit(
{
episodeIds: JSON.stringify(episodeIds),
spaceId,
action: selectedSpaces.includes(spaceId) ? "remove" : "assign",
},
{
method: "post",
action: "/api/v1/episodes/assign-space",
encType: "application/json",
},
);
}
// Call the callback if provided
if (onSpaceChange) {
onSpaceChange(newSelectedSpaces);
}
};
const selectedSpaceObjects = spaces.filter((space) =>
selectedSpaces.includes(space.id),
);
const getTrigger = () => {
if (selectedSpaceObjects?.length === 1) {
return (
<>
<Project size={14} /> {selectedSpaceObjects[0].name}
</>
);
}
if (selectedSpaceObjects?.length > 1) {
return (
<>
<Project size={14} /> {selectedSpaceObjects.length} Spaces
</>
);
}
return (
<>
{" "}
<Project size={14} />
Spaces
</>
);
};
return (
<div className={cn("flex flex-wrap items-center gap-2", className)}>
{/* + button to add more spaces */}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="secondary"
size="sm"
role="combobox"
aria-expanded={open}
className="h-7 gap-1 rounded"
>
{getTrigger()}
</Button>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent className="w-[250px] p-0" align="end">
<Command>
<CommandInput placeholder="Search spaces..." />
<CommandList>
<CommandEmpty>No spaces found.</CommandEmpty>
<CommandGroup>
{spaces.map((space) => (
<CommandItem
key={space.id}
value={space.name}
onSelect={() => handleSpaceToggle(space.id)}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedSpaces.includes(space.id)
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="text-sm">{space.name}</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</PopoverPortal>
</Popover>
</div>
);
}