- Add Ratings tab (4th tab) with rate form, personal history, community section - Overview tab: conditionally render taskCount, avgCostUsd, lastUsedAt rows - Import StarRating, Separator, Skeleton, Textarea, EmptyState, TooltipProvider - saveRatingMutation calls addRating, invalidates ratings query on success - ratingsQuery loads personal history with loading/empty/error states
665 lines
25 KiB
TypeScript
665 lines
25 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useParams, Link } from "react-router-dom";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { diffLines } from "diff";
|
|
import { ChevronLeft, Star, Download, RotateCcw, Trash2 } from "lucide-react";
|
|
import { useCompany } from "@/context/CompanyContext";
|
|
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
|
|
import { useToast } from "@/context/ToastContext";
|
|
import { skillRegistryApi } from "@/api/skillRegistry";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import { cn, relativeTime } from "@/lib/utils";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { PageTabBar } from "@/components/PageTabBar";
|
|
import { PageSkeleton } from "@/components/PageSkeleton";
|
|
import { EmptyState } from "@/components/EmptyState";
|
|
import { StarRating } from "@/components/StarRating";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* VersionDiff component */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function VersionDiff({ oldContent, newContent }: { oldContent: string; newContent: string }) {
|
|
const parts = diffLines(oldContent, newContent);
|
|
return (
|
|
<pre
|
|
role="region"
|
|
aria-label="Version diff"
|
|
className="text-xs font-mono rounded border border-border bg-muted/30 p-4 overflow-x-auto"
|
|
>
|
|
{parts.map((part, i) => (
|
|
<span
|
|
key={i}
|
|
className={cn(
|
|
part.added && "bg-green-500/20 text-green-700 dark:text-green-400",
|
|
part.removed && "bg-red-500/20 text-red-700 dark:text-red-400",
|
|
!part.added && !part.removed && "text-muted-foreground",
|
|
)}
|
|
>
|
|
{part.value}
|
|
</span>
|
|
))}
|
|
</pre>
|
|
);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* SkillDetail page */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function SkillDetail() {
|
|
const { skillId: rawSkillId } = useParams<{ skillId: string }>();
|
|
const skillId = rawSkillId ? decodeURIComponent(rawSkillId) : "";
|
|
|
|
const { selectedCompany } = useCompany();
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
const { pushToast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
|
|
const [detailTab, setDetailTab] = useState("overview");
|
|
const [versionA, setVersionA] = useState<string>("");
|
|
const [versionB, setVersionB] = useState<string>("");
|
|
|
|
// Rating form state
|
|
const [pendingStars, setPendingStars] = useState(0);
|
|
const [pendingNote, setPendingNote] = useState("");
|
|
|
|
// Install dialog state — used for both install and update flows
|
|
const [installDialog, setInstallDialog] = useState<{
|
|
skillId: string;
|
|
isUpdate: boolean;
|
|
agentSkillsDir?: string;
|
|
} | null>(null);
|
|
|
|
// Uninstall confirmation dialog state
|
|
const [uninstallDialog, setUninstallDialog] = useState<{ skillId: string } | null>(null);
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Data queries */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
const { data: skill, isLoading, isError } = useQuery({
|
|
queryKey: queryKeys.skillRegistry.detail(skillId),
|
|
queryFn: () => skillRegistryApi.getById(skillId),
|
|
enabled: !!skillId,
|
|
});
|
|
|
|
const { data: versions = [] } = useQuery({
|
|
queryKey: queryKeys.skillRegistry.versions(skillId),
|
|
queryFn: () => skillRegistryApi.getVersions(skillId),
|
|
enabled: !!skillId,
|
|
});
|
|
|
|
const {
|
|
data: ratings = [],
|
|
isLoading: ratingsLoading,
|
|
isError: ratingsError,
|
|
} = useQuery({
|
|
queryKey: ["skill-ratings", skillId],
|
|
queryFn: () => skillRegistryApi.getRatings(skillId),
|
|
enabled: !!skillId,
|
|
});
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Breadcrumbs */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
useEffect(() => {
|
|
setBreadcrumbs([
|
|
{ label: selectedCompany?.name ?? "Workspace", href: "/dashboard" },
|
|
{ label: "Skills", href: "../skills" },
|
|
{ label: skill?.name ?? "Skill" },
|
|
]);
|
|
}, [selectedCompany?.name, skill?.name, setBreadcrumbs]);
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Mutations */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
const invalidateSkill = () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.detail(skillId) });
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list });
|
|
};
|
|
|
|
const installMutation = useMutation({
|
|
mutationFn: ({ agentSkillsDir }: { agentSkillsDir: string }) =>
|
|
skillRegistryApi.install(skillId, agentSkillsDir),
|
|
onSuccess: () => {
|
|
invalidateSkill();
|
|
setInstallDialog(null);
|
|
pushToast({ title: "Skill installed", tone: "success" });
|
|
},
|
|
onError: (err: Error) => {
|
|
pushToast({ title: "Install failed", body: err.message, tone: "error" });
|
|
},
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ agentSkillsDir }: { agentSkillsDir: string }) =>
|
|
skillRegistryApi.install(skillId, agentSkillsDir),
|
|
onSuccess: () => {
|
|
invalidateSkill();
|
|
setInstallDialog(null);
|
|
pushToast({ title: "Skill updated", tone: "success" });
|
|
},
|
|
onError: (err: Error) => {
|
|
pushToast({ title: "Update failed", body: err.message, tone: "error" });
|
|
},
|
|
});
|
|
|
|
const rollbackMutation = useMutation({
|
|
mutationFn: ({ versionId, agentSkillsDir }: { versionId: string; agentSkillsDir: string }) =>
|
|
skillRegistryApi.rollback(skillId, versionId, agentSkillsDir),
|
|
onSuccess: () => {
|
|
invalidateSkill();
|
|
pushToast({ title: "Rolled back to previous version", tone: "success" });
|
|
},
|
|
onError: (err: Error) => {
|
|
pushToast({ title: "Rollback failed", body: err.message, tone: "error" });
|
|
},
|
|
});
|
|
|
|
const removeMutation = useMutation({
|
|
mutationFn: () => skillRegistryApi.remove(skillId),
|
|
onSuccess: () => {
|
|
invalidateSkill();
|
|
setUninstallDialog(null);
|
|
pushToast({ title: "Skill uninstalled", tone: "success" });
|
|
},
|
|
onError: (err: Error) => {
|
|
pushToast({ title: "Uninstall failed", body: err.message, tone: "error" });
|
|
},
|
|
});
|
|
|
|
const saveRatingMutation = useMutation({
|
|
mutationFn: ({ stars, note }: { stars: number; note: string }) =>
|
|
skillRegistryApi.addRating(skillId, {
|
|
stars,
|
|
versionId: skill?.activeVersionId ?? undefined,
|
|
note: note.trim() || undefined,
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["skill-ratings", skillId] });
|
|
setPendingStars(0);
|
|
setPendingNote("");
|
|
pushToast({ title: "Rating saved", tone: "success" });
|
|
},
|
|
onError: (err: Error) => {
|
|
pushToast({ title: "Failed to save rating", body: err.message, tone: "error" });
|
|
},
|
|
});
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Loading / error states */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<main className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
<PageSkeleton variant="detail" />
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<main className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
<p className="text-sm text-destructive">
|
|
Failed to load skills. Check that the skill registry backend is running and try again.
|
|
</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (!skill) {
|
|
return (
|
|
<main className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
<p className="text-sm text-muted-foreground">Skill not found.</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const isInstalled = !!skill.activeVersionId;
|
|
const isMutating =
|
|
installMutation.isPending ||
|
|
updateMutation.isPending ||
|
|
rollbackMutation.isPending ||
|
|
removeMutation.isPending;
|
|
|
|
/* ---------------------------------------------------------------- */
|
|
/* Render */
|
|
/* ---------------------------------------------------------------- */
|
|
|
|
return (
|
|
<main className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
{/* Back link */}
|
|
<Link
|
|
to="../skills"
|
|
className="text-sm text-muted-foreground hover:underline flex items-center gap-1"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" /> Skills
|
|
</Link>
|
|
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-xl font-semibold">{skill.name}</h1>
|
|
<Badge variant="secondary" className="text-xs mt-1">
|
|
{skill.sourceId}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex gap-2 flex-shrink-0">
|
|
{!isInstalled && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={isMutating}
|
|
onClick={() => setInstallDialog({ skillId, isUpdate: false })}
|
|
>
|
|
<Download className="h-3.5 w-3.5 mr-1" />
|
|
Install skill
|
|
</Button>
|
|
)}
|
|
{isInstalled && (
|
|
<>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={isMutating}
|
|
onClick={() => setInstallDialog({ skillId, isUpdate: true })}
|
|
>
|
|
Update skill
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
disabled={isMutating}
|
|
onClick={() => setUninstallDialog({ skillId })}
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
|
Uninstall
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
{skill.description && (
|
|
<p className="text-sm text-muted-foreground">{skill.description}</p>
|
|
)}
|
|
|
|
{/* Detail tabs */}
|
|
<TooltipProvider>
|
|
<Tabs value={detailTab} onValueChange={setDetailTab}>
|
|
<PageTabBar
|
|
items={[
|
|
{ value: "overview", label: "Overview" },
|
|
{ value: "versions", label: "Versions" },
|
|
{ value: "diff", label: "Diff" },
|
|
{ value: "ratings", label: "Ratings" },
|
|
]}
|
|
value={detailTab}
|
|
onValueChange={setDetailTab}
|
|
align="start"
|
|
/>
|
|
|
|
{/* Overview tab */}
|
|
<TabsContent value="overview" className="space-y-1 pt-4">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Installs</span>
|
|
<span className="text-sm font-semibold">{skill.ratingCount ?? "\u2014"}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Average rating</span>
|
|
<span className="flex items-center gap-1 text-sm font-semibold">
|
|
{skill.averageRating != null ? (
|
|
<>
|
|
<Star className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
|
|
{skill.averageRating.toFixed(1)}
|
|
</>
|
|
) : (
|
|
"No ratings yet"
|
|
)}
|
|
</span>
|
|
</div>
|
|
{skill.averageRating != null && skill.ratingCount != null && (
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Community</span>
|
|
<span className="text-sm font-semibold">({skill.ratingCount} ratings)</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Source</span>
|
|
<span className="text-xs font-mono text-muted-foreground">{skill.sourceId}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Active version</span>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{skill.activeVersionId
|
|
? `v${skill.activeVersionId.split("@").pop() ?? skill.activeVersionId}`
|
|
: "\u2014"}
|
|
</span>
|
|
</div>
|
|
{/* Usage stats — Phase 12 */}
|
|
{skill.taskCount != null && skill.taskCount > 0 && (
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Tasks completed</span>
|
|
<span className="text-sm font-semibold">{skill.taskCount}</span>
|
|
</div>
|
|
)}
|
|
{skill.avgCostUsd != null && (
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Avg task cost</span>
|
|
<span className="text-sm font-semibold">${skill.avgCostUsd.toFixed(4)}</span>
|
|
</div>
|
|
)}
|
|
{skill.lastUsedAt != null && (
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Last used</span>
|
|
<span className="text-sm font-semibold">{relativeTime(new Date(skill.lastUsedAt))}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Versions tab */}
|
|
<TabsContent value="versions" className="pt-4">
|
|
{versions.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No versions fetched yet.</p>
|
|
) : (
|
|
<ScrollArea className="max-h-96">
|
|
<div className="space-y-1">
|
|
{versions.map((v) => (
|
|
<div
|
|
key={v.id}
|
|
className="flex items-center justify-between py-2 px-2 rounded hover:bg-accent/50"
|
|
>
|
|
<div>
|
|
<span className="text-xs font-mono text-muted-foreground">{v.version}</span>
|
|
<span className="text-xs text-muted-foreground ml-2">
|
|
{relativeTime(new Date(v.fetchedAt))}
|
|
</span>
|
|
</div>
|
|
{v.id === skill.activeVersionId && (
|
|
<Badge variant="outline" className="text-xs">
|
|
Active
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* Diff tab */}
|
|
<TabsContent value="diff" className="pt-4 space-y-4">
|
|
<div className="flex gap-2 mb-4">
|
|
<Select value={versionA} onValueChange={setVersionA}>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue placeholder="Select version A" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{versions.map((v) => (
|
|
<SelectItem key={v.id} value={v.id}>
|
|
{v.version}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={versionB} onValueChange={setVersionB}>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue placeholder="Select version B" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{versions.map((v) => (
|
|
<SelectItem key={v.id} value={v.id}>
|
|
{v.version}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{versionA && versionB ? (
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-muted-foreground">
|
|
Version diff requires file content endpoint (coming in a future update).
|
|
</p>
|
|
<VersionDiff oldContent="" newContent="" />
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">
|
|
Select two versions above to compare them.
|
|
</p>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* Ratings tab */}
|
|
<TabsContent value="ratings" className="pt-4 space-y-6">
|
|
{/* Section 1: Rate this skill */}
|
|
<div className="space-y-4">
|
|
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
Rate this skill
|
|
</h2>
|
|
<div className="space-y-2">
|
|
<StarRating value={pendingStars} onChange={setPendingStars} size="md" />
|
|
<Textarea
|
|
placeholder="Add a note about this version\u2026"
|
|
value={pendingNote}
|
|
onChange={(e) => setPendingNote(e.target.value)}
|
|
rows={3}
|
|
className="resize-none"
|
|
/>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="default"
|
|
disabled={pendingStars === 0 || saveRatingMutation.isPending}
|
|
onClick={() => {
|
|
if (pendingStars === 0) return;
|
|
saveRatingMutation.mutate({ stars: pendingStars, note: pendingNote });
|
|
}}
|
|
>
|
|
{saveRatingMutation.isPending ? "Saving\u2026" : "Save rating"}
|
|
</Button>
|
|
{saveRatingMutation.isError && (
|
|
<p className="text-sm text-destructive">
|
|
{(saveRatingMutation.error as Error).message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Section 2: Your Rating History */}
|
|
<div className="space-y-3">
|
|
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
Your Rating History
|
|
</h2>
|
|
{ratingsError && (
|
|
<p className="text-sm text-destructive">Failed to load ratings.</p>
|
|
)}
|
|
{ratingsLoading && (
|
|
<div aria-hidden="true" className="space-y-2">
|
|
<Skeleton className="h-8 w-full" />
|
|
<Skeleton className="h-8 w-full" />
|
|
<Skeleton className="h-8 w-full" />
|
|
</div>
|
|
)}
|
|
{!ratingsLoading && !ratingsError && ratings.length === 0 && (
|
|
<EmptyState
|
|
icon={Star}
|
|
message="No ratings yet"
|
|
/>
|
|
)}
|
|
{!ratingsLoading && !ratingsError && ratings.length > 0 && (
|
|
<ScrollArea className="max-h-80">
|
|
<div className="space-y-1">
|
|
{ratings.map((r) => (
|
|
<div
|
|
key={r.id}
|
|
className="flex items-start justify-between py-2 px-2 rounded hover:bg-accent/50"
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<StarRating value={r.stars} readonly size="sm" />
|
|
{r.versionId && (
|
|
<Badge variant="outline" className="text-xs font-mono">
|
|
{r.versionId.slice(-7)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{r.note && (
|
|
<p className="text-sm text-muted-foreground">{r.note}</p>
|
|
)}
|
|
</div>
|
|
<span className="text-xs text-muted-foreground shrink-0 ml-4">
|
|
{relativeTime(new Date(r.createdAt))}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Section 3: Community Ratings */}
|
|
<div className="space-y-3">
|
|
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
Community Ratings
|
|
</h2>
|
|
{skill.averageRating != null ? (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Average</span>
|
|
<span className="flex items-center gap-2">
|
|
<StarRating value={Math.round(skill.averageRating)} readonly size="sm" />
|
|
<span className="text-sm font-semibold">{skill.averageRating.toFixed(1)}</span>
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between py-2">
|
|
<span className="text-xs text-muted-foreground">Ratings</span>
|
|
<span className="text-sm font-semibold">{skill.ratingCount}</span>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">No community ratings available.</p>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</TooltipProvider>
|
|
|
|
{/* Install / Update dialog */}
|
|
<Dialog open={!!installDialog} onOpenChange={(open) => !open && setInstallDialog(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{installDialog?.isUpdate ? "Update skill" : "Install skill"}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{installDialog?.isUpdate
|
|
? "Update this skill to the latest version for your agents."
|
|
: "Install this skill to add new capabilities to your agents."}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="py-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
Agent skills directory is required to install. Enter the path to the agent's
|
|
skills directory.
|
|
</p>
|
|
<input
|
|
type="text"
|
|
className="mt-2 w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
|
placeholder="/path/to/agent/skills"
|
|
value={installDialog?.agentSkillsDir ?? ""}
|
|
onChange={(e) =>
|
|
setInstallDialog((d) => d ? { ...d, agentSkillsDir: e.target.value } : null)
|
|
}
|
|
/>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setInstallDialog(null)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
disabled={!installDialog?.agentSkillsDir || isMutating}
|
|
onClick={() => {
|
|
if (!installDialog?.agentSkillsDir) return;
|
|
if (installDialog.isUpdate) {
|
|
updateMutation.mutate({ agentSkillsDir: installDialog.agentSkillsDir });
|
|
} else {
|
|
installMutation.mutate({ agentSkillsDir: installDialog.agentSkillsDir });
|
|
}
|
|
}}
|
|
>
|
|
{installDialog?.isUpdate
|
|
? updateMutation.isPending
|
|
? "Updating\u2026"
|
|
: "Update skill"
|
|
: installMutation.isPending
|
|
? "Installing\u2026"
|
|
: "Install skill"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Uninstall confirmation dialog */}
|
|
<Dialog open={!!uninstallDialog} onOpenChange={(open) => !open && setUninstallDialog(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Uninstall skill?</DialogTitle>
|
|
<DialogDescription>
|
|
This will remove the skill files from the agent's directory. You can reinstall
|
|
it later.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setUninstallDialog(null)}>
|
|
Keep skill
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
disabled={removeMutation.isPending}
|
|
onClick={() => removeMutation.mutate()}
|
|
>
|
|
{removeMutation.isPending ? "Uninstalling\u2026" : "Yes, uninstall"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
);
|
|
}
|