diff --git a/ui/src/pages/SkillDetail.tsx b/ui/src/pages/SkillDetail.tsx index 321b804d..9d000cf6 100644 --- a/ui/src/pages/SkillDetail.tsx +++ b/ui/src/pages/SkillDetail.tsx @@ -27,9 +27,20 @@ import { 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 */ @@ -76,6 +87,10 @@ export function SkillDetail() { const [versionA, setVersionA] = useState(""); const [versionB, setVersionB] = useState(""); + // 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; @@ -102,6 +117,16 @@ export function SkillDetail() { enabled: !!skillId, }); + const { + data: ratings = [], + isLoading: ratingsLoading, + isError: ratingsError, + } = useQuery({ + queryKey: ["skill-ratings", skillId], + queryFn: () => skillRegistryApi.getRatings(skillId), + enabled: !!skillId, + }); + /* ---------------------------------------------------------------- */ /* Breadcrumbs */ /* ---------------------------------------------------------------- */ @@ -173,6 +198,24 @@ export function SkillDetail() { }, }); + 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 */ /* ---------------------------------------------------------------- */ @@ -274,131 +317,269 @@ export function SkillDetail() { )} {/* Detail tabs */} - - + + + - {/* Overview tab */} - -
-
- Installs - {skill.ratingCount ?? "\u2014"} -
-
- Average rating - - {skill.averageRating != null ? ( - <> - - {skill.averageRating.toFixed(1)} - - ) : ( - "No ratings yet" - )} - -
- {skill.averageRating != null && skill.ratingCount != null && ( + {/* Overview tab */} + +
- Community - ({skill.ratingCount} ratings) + Installs + {skill.ratingCount ?? "\u2014"}
- )} -
- Source - {skill.sourceId} +
+ Average rating + + {skill.averageRating != null ? ( + <> + + {skill.averageRating.toFixed(1)} + + ) : ( + "No ratings yet" + )} + +
+ {skill.averageRating != null && skill.ratingCount != null && ( +
+ Community + ({skill.ratingCount} ratings) +
+ )} +
+ Source + {skill.sourceId} +
+
+ Active version + + {skill.activeVersionId + ? `v${skill.activeVersionId.split("@").pop() ?? skill.activeVersionId}` + : "\u2014"} + +
+ {/* Usage stats — Phase 12 */} + {skill.taskCount != null && skill.taskCount > 0 && ( +
+ Tasks completed + {skill.taskCount} +
+ )} + {skill.avgCostUsd != null && ( +
+ Avg task cost + ${skill.avgCostUsd.toFixed(4)} +
+ )} + {skill.lastUsedAt != null && ( +
+ Last used + {relativeTime(new Date(skill.lastUsedAt))} +
+ )}
-
- Active version - - {skill.activeVersionId - ? `v${skill.activeVersionId.split("@").pop() ?? skill.activeVersionId}` - : "\u2014"} - -
-
-
+ - {/* Versions tab */} - - {versions.length === 0 ? ( -

No versions fetched yet.

- ) : ( - -
- {versions.map((v) => ( -
-
- {v.version} - - {relativeTime(new Date(v.fetchedAt))} - + {/* Versions tab */} + + {versions.length === 0 ? ( +

No versions fetched yet.

+ ) : ( + +
+ {versions.map((v) => ( +
+
+ {v.version} + + {relativeTime(new Date(v.fetchedAt))} + +
+ {v.id === skill.activeVersionId && ( + + Active + + )}
- {v.id === skill.activeVersionId && ( - - Active - - )} -
- ))} -
- - )} - + ))} +
+ + )} + - {/* Diff tab */} - -
- - -
- {versionA && versionB ? ( -
-

- Version diff requires file content endpoint (coming in a future update). -

- + {/* Diff tab */} + +
+ +
- ) : ( -

- Select two versions above to compare them. -

- )} -
- + {versionA && versionB ? ( +
+

+ Version diff requires file content endpoint (coming in a future update). +

+ +
+ ) : ( +

+ Select two versions above to compare them. +

+ )} + + + {/* Ratings tab */} + + {/* Section 1: Rate this skill */} +
+

+ Rate this skill +

+
+ +