"use client"; import React, { useMemo } from "react"; import { Package, AlertTriangle, CheckCircle, RefreshCw, AlertCircle, Zap, } from "lucide-react"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Separator } from "~/components/ui/separator"; import { cn } from "~/lib/utils"; import type { ExperimentStep, ActionDefinition, } from "~/lib/experiment-designer/types"; /* -------------------------------------------------------------------------- */ /* Types */ /* -------------------------------------------------------------------------- */ export interface PluginDependency { pluginId: string; version: string; robotId?: string; name?: string; status: "available" | "missing" | "outdated" | "error"; installedVersion?: string; actionCount: number; driftedActionCount: number; } export interface ActionSignatureDrift { actionId: string; actionName: string; stepId: string; stepName: string; type: string; pluginId?: string; pluginVersion?: string; driftType: "missing_definition" | "schema_changed" | "version_mismatch"; details?: string; } export interface DependencyInspectorProps { steps: ExperimentStep[]; /** * Map of action instance ID to signature drift information */ actionSignatureDrift: Set; /** * Available action definitions from registry */ actionDefinitions: ActionDefinition[]; /** * Study plugins with name and metadata */ studyPlugins?: Array<{ id: string; robotId: string; name: string; version: string; }>; /** * Called when user wants to reconcile a drifted action */ onReconcileAction?: (actionId: string) => void; /** * Called when user wants to refresh plugin dependencies */ onRefreshDependencies?: () => void; /** * Called when user wants to install a missing plugin */ onInstallPlugin?: (pluginId: string) => void; className?: string; } /* -------------------------------------------------------------------------- */ /* Utility Functions */ /* -------------------------------------------------------------------------- */ function extractPluginDependencies( steps: ExperimentStep[], actionDefinitions: ActionDefinition[], driftedActions: Set, studyPlugins?: Array<{ id: string; robotId: string; name: string; version: string; }>, ): PluginDependency[] { const dependencyMap = new Map(); // Collect all plugin actions used in the experiment steps.forEach((step) => { step.actions.forEach((action) => { if (action.source.kind === "plugin" && action.source.pluginId) { const key = `${action.source.pluginId}@${action.source.pluginVersion}`; if (!dependencyMap.has(key)) { dependencyMap.set(key, { pluginId: action.source.pluginId, version: action.source.pluginVersion ?? "unknown", status: "available", // Will be updated below actionCount: 0, driftedActionCount: 0, }); } const dep = dependencyMap.get(key)!; dep.actionCount++; if (driftedActions.has(action.id)) { dep.driftedActionCount++; } } }); }); // Update status based on available definitions dependencyMap.forEach((dep) => { const availableActions = actionDefinitions.filter( (def) => def.source.kind === "plugin" && def.source.pluginId === dep.pluginId, ); if (availableActions.length === 0) { dep.status = "missing"; } else { // Check if we have the exact version const exactVersion = availableActions.find( (def) => def.source.pluginVersion === dep.version, ); if (!exactVersion) { dep.status = "outdated"; // Get the installed version const anyVersion = availableActions[0]; dep.installedVersion = anyVersion?.source.pluginVersion; } else { dep.status = "available"; dep.installedVersion = dep.version; } // Set plugin name from studyPlugins if available if (availableActions[0]) { const pluginMeta = studyPlugins?.find( (p) => p.robotId === dep.pluginId, ); dep.name = pluginMeta?.name ?? dep.pluginId; } } }); return Array.from(dependencyMap.values()).sort((a, b) => a.pluginId.localeCompare(b.pluginId), ); } function extractActionDrifts( steps: ExperimentStep[], actionDefinitions: ActionDefinition[], driftedActions: Set, ): ActionSignatureDrift[] { const drifts: ActionSignatureDrift[] = []; steps.forEach((step) => { step.actions.forEach((action) => { if (driftedActions.has(action.id)) { const definition = actionDefinitions.find( (def) => def.type === action.type, ); let driftType: ActionSignatureDrift["driftType"] = "missing_definition"; let details = ""; if (!definition) { driftType = "missing_definition"; details = `Action definition for type '${action.type}' not found`; } else if ( action.source.pluginId && action.source.pluginVersion !== definition.source.pluginVersion ) { driftType = "version_mismatch"; details = `Expected v${action.source.pluginVersion}, found v${definition.source.pluginVersion}`; } else { driftType = "schema_changed"; details = "Action schema or execution parameters have changed"; } drifts.push({ actionId: action.id, actionName: action.name, stepId: step.id, stepName: step.name, type: action.type, pluginId: action.source.pluginId, pluginVersion: action.source.pluginVersion, driftType, details, }); } }); }); return drifts; } /* -------------------------------------------------------------------------- */ /* Plugin Dependency Item */ /* -------------------------------------------------------------------------- */ interface PluginDependencyItemProps { dependency: PluginDependency; onInstall?: (pluginId: string) => void; } function PluginDependencyItem({ dependency, onInstall, }: PluginDependencyItemProps) { const statusConfig = { available: { icon: CheckCircle, color: "text-green-600 dark:text-green-400", badgeVariant: "outline" as const, badgeColor: "border-green-300 text-green-700 dark:text-green-300", }, missing: { icon: AlertCircle, color: "text-red-600 dark:text-red-400", badgeVariant: "destructive" as const, badgeColor: "", }, outdated: { icon: AlertTriangle, color: "text-amber-600 dark:text-amber-400", badgeVariant: "secondary" as const, badgeColor: "", }, error: { icon: AlertTriangle, color: "text-red-600 dark:text-red-400", badgeVariant: "destructive" as const, badgeColor: "", }, }; const config = statusConfig[dependency.status]; const IconComponent = config.icon; return (
{dependency.name ?? dependency.pluginId} {dependency.status}
v{dependency.version} {dependency.installedVersion && dependency.installedVersion !== dependency.version && ( (installed: v{dependency.installedVersion}) )} • {dependency.actionCount} actions {dependency.driftedActionCount > 0 && ( • {dependency.driftedActionCount} drifted )}
{dependency.status === "missing" && onInstall && ( )}
); } /* -------------------------------------------------------------------------- */ /* Action Drift Item */ /* -------------------------------------------------------------------------- */ interface ActionDriftItemProps { drift: ActionSignatureDrift; onReconcile?: (actionId: string) => void; } function ActionDriftItem({ drift, onReconcile }: ActionDriftItemProps) { const driftConfig = { missing_definition: { icon: AlertCircle, color: "text-red-600 dark:text-red-400", badgeVariant: "destructive" as const, label: "Missing", }, schema_changed: { icon: AlertTriangle, color: "text-amber-600 dark:text-amber-400", badgeVariant: "secondary" as const, label: "Schema Changed", }, version_mismatch: { icon: AlertTriangle, color: "text-blue-600 dark:text-blue-400", badgeVariant: "outline" as const, label: "Version Mismatch", }, }; const config = driftConfig[drift.driftType]; const IconComponent = config.icon; return (

{drift.actionName}

in {drift.stepName} • {drift.type}

{config.label}
{drift.details && (

{drift.details}

)} {drift.pluginId && (
{drift.pluginId} {drift.pluginVersion && `@${drift.pluginVersion}`}
)}
{onReconcile && ( )}
); } /* -------------------------------------------------------------------------- */ /* DependencyInspector Component */ /* -------------------------------------------------------------------------- */ export function DependencyInspector({ steps, actionSignatureDrift, actionDefinitions, studyPlugins, onReconcileAction, onRefreshDependencies, onInstallPlugin, className, }: DependencyInspectorProps) { const dependencies = useMemo( () => extractPluginDependencies( steps, actionDefinitions, actionSignatureDrift, studyPlugins, ), [steps, actionDefinitions, actionSignatureDrift, studyPlugins], ); const drifts = useMemo( () => extractActionDrifts(steps, actionDefinitions, actionSignatureDrift), [steps, actionDefinitions, actionSignatureDrift], ); // Count core vs plugin actions const actionCounts = useMemo(() => { let core = 0; let plugin = 0; steps.forEach((step) => { step.actions.forEach((action) => { if (action.source.kind === "plugin") { plugin++; } else { core++; } }); }); return { core, plugin, total: core + plugin }; }, [steps]); const hasIssues = dependencies.some((d) => d.status !== "available") || drifts.length > 0; return (
Dependencies
{hasIssues ? ( Issues ) : ( Healthy )} {onRefreshDependencies && ( )}
{/* Action Summary */}

Action Summary

{actionCounts.core} core {actionCounts.plugin} plugin {actionCounts.total} total
{/* Plugin Dependencies */} {dependencies.length > 0 && ( <>

Plugin Dependencies ({dependencies.length})

{dependencies.map((dep) => ( ))}
)} {/* Action Signature Drifts */} {drifts.length > 0 && ( <>

Action Drift ({drifts.length})

{drifts.map((drift) => ( ))}
)} {/* Empty State */} {dependencies.length === 0 && drifts.length === 0 && (

No plugin dependencies

This experiment uses only core actions

)} {/* Healthy State */} {dependencies.length > 0 && !hasIssues && (

All dependencies healthy

No drift or missing plugins detected

)}
); }