"use client"; import { Puzzle, Search, Filter, ExternalLink, Download, Shield, User, Calendar, Database, } from "lucide-react"; import React from "react"; import { formatDistanceToNow } from "date-fns"; import { toast } from "sonner"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "~/components/ui/card"; import { Input } from "~/components/ui/input"; import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; import { PageHeader } from "~/components/ui/page-header"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { useStudyContext } from "~/lib/study-context"; import { api } from "~/trpc/react"; interface PluginStoreItem { id: string; robotId: string | null; name: string; version: string; description: string | null; author: string | null; repositoryUrl: string | null; trustLevel: "official" | "verified" | "community" | null; status: "active" | "deprecated" | "disabled"; createdAt: Date; updatedAt: Date; metadata: unknown; } const trustLevelConfig = { official: { label: "Official", className: "bg-blue-100 text-blue-800", icon: Shield, description: "Official HRIStudio plugin", }, verified: { label: "Verified", className: "bg-green-100 text-green-800", icon: Shield, description: "Verified by the community", }, community: { label: "Community", className: "bg-yellow-100 text-yellow-800", icon: User, description: "Community contributed", }, }; function PluginCard({ plugin, onInstall, repositoryName, isInstalled, }: { plugin: PluginStoreItem; onInstall: (pluginId: string) => void; repositoryName?: string; isInstalled?: boolean; }) { const trustLevel = plugin.trustLevel; const trustConfig = trustLevel ? trustLevelConfig[trustLevel] : null; const TrustIcon = trustConfig?.icon ?? User; return (
{plugin.name}
v{plugin.version} {trustConfig && ( {trustConfig.label} )}
{plugin.description && ( {plugin.description} )}
{plugin.author && (
{plugin.author}
)}
Updated{" "} {formatDistanceToNow(plugin.updatedAt, { addSuffix: true })}
{repositoryName && (
{repositoryName}
)}
{plugin.repositoryUrl && ( )}
{plugin.status !== "active" && ( {plugin.status} )}
); } export function PluginStoreBrowse() { const [searchTerm, setSearchTerm] = React.useState(""); const [statusFilter, setStatusFilter] = React.useState("all"); const [trustLevelFilter, setTrustLevelFilter] = React.useState("all"); const { selectedStudyId } = useStudyContext(); // Get enabled repositories first const { data: repositories } = api.admin.repositories.list.useQuery( { isEnabled: true, limit: 100, }, { refetchOnWindowFocus: false, }, ) as { data: Array<{ id: string; url: string; name: string }> | undefined }; // Get installed plugins for current study const { data: installedPlugins } = api.robots.plugins.getStudyPlugins.useQuery( { studyId: selectedStudyId!, }, { enabled: !!selectedStudyId, refetchOnWindowFocus: false, }, ); const { data: availablePlugins, isLoading, error, } = api.robots.plugins.list.useQuery( { status: statusFilter === "all" ? undefined : (statusFilter as "active" | "deprecated" | "disabled"), limit: 50, }, { refetchOnWindowFocus: false, enabled: Boolean(repositories?.length), }, ); const utils = api.useUtils(); const installPluginMutation = api.robots.plugins.install.useMutation({ onSuccess: () => { toast.success("Plugin installed successfully!"); // Invalidate both plugin queries to refresh the UI void utils.robots.plugins.list.invalidate(); void utils.robots.plugins.getStudyPlugins.invalidate(); }, onError: (error) => { toast.error(error.message || "Failed to install plugin"); }, }); // Get study data for breadcrumbs const { data: studyData } = api.studies.get.useQuery( { id: selectedStudyId! }, { enabled: !!selectedStudyId }, ); // Set breadcrumbs useBreadcrumbsEffect([ { label: "Dashboard", href: "/dashboard" }, { label: "Studies", href: "/studies" }, ...(selectedStudyId && studyData ? [ { label: studyData.name, href: `/studies/${selectedStudyId}` }, { label: "Plugins", href: "/plugins" }, { label: "Browse" }, ] : [{ label: "Plugins", href: "/plugins" }, { label: "Browse" }]), ]); const handleInstall = React.useCallback( (pluginId: string) => { if (!selectedStudyId) { toast.error("Please select a study first"); return; } installPluginMutation.mutate({ studyId: selectedStudyId, pluginId, }); }, [selectedStudyId, installPluginMutation], ); // Transform and filter plugins const filteredPlugins = React.useMemo(() => { if (!availablePlugins) return []; return availablePlugins.filter((plugin) => { const matchesSearch = searchTerm === "" || plugin.name.toLowerCase().includes(searchTerm.toLowerCase()) || (plugin.description?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false) || (plugin.author?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false); const matchesStatus = statusFilter === "all" || plugin.status === statusFilter; const matchesTrustLevel = trustLevelFilter === "all" || plugin.trustLevel === trustLevelFilter; return matchesSearch && matchesStatus && matchesTrustLevel; }); }, [availablePlugins, searchTerm, statusFilter, trustLevelFilter]); // Create a set of installed plugin IDs for quick lookup const installedPluginIds = React.useMemo(() => { if (!installedPlugins) return new Set(); return new Set(installedPlugins.map((p) => p.plugin.id)); }, [installedPlugins]); // Status filter options const statusOptions = [ { label: "All Statuses", value: "all" }, { label: "Active", value: "active" }, { label: "Deprecated", value: "deprecated" }, { label: "Disabled", value: "disabled" }, ]; // Trust level filter options const trustLevelOptions = [ { label: "All Trust Levels", value: "all" }, { label: "Official", value: "official" }, { label: "Verified", value: "verified" }, { label: "Community", value: "community" }, ]; return (
{!selectedStudyId && (

Select a study from the sidebar to install plugins

)} {repositories?.length === 0 && (

No Plugin Repositories Configured

Contact your administrator to add plugin repositories.

)} {/* Search and Filters */}
setSearchTerm(e.target.value)} className="pl-9" />
{/* Loading State */} {isLoading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)} {/* Error State */} {error && (

Failed to Load Plugins

{error.message || "An error occurred while loading the plugin store."}

)} {/* Plugin Grid */} {!isLoading && !error && ( <> {filteredPlugins.length === 0 ? (

No Plugins Found

{searchTerm || statusFilter !== "all" || trustLevelFilter !== "all" ? "Try adjusting your search or filters" : "No plugins are currently available"}

) : (
{filteredPlugins.map((plugin) => { // Find repository for this plugin by checking metadata const repository = repositories?.find((repo) => { // First try to match by URL if (plugin.repositoryUrl?.includes(repo.url)) { return true; } // Then try to match by repository ID in metadata if available const metadata = plugin.metadata as { repositoryId?: string; } | null; return metadata?.repositoryId === repo.id; }); return ( ); })}
)} {/* Results Count */} {filteredPlugins.length > 0 && (
Showing {filteredPlugins.length} plugin {filteredPlugins.length !== 1 ? "s" : ""} {availablePlugins && filteredPlugins.length < availablePlugins.length && ` of ${availablePlugins.length} total`}
)} )}
); }