From 86d4de87e3ee79aa52590e3235a6d557acdd092e Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Wed, 1 Apr 2026 04:23:04 +0200 Subject: [PATCH] feat(12-02): StarRating component, API extensions, DesignGuide entry - Create StarRating component with interactive/readonly modes, amber stars, size sm/md - Add PersonalRating type and taskCount/avgCostUsd/lastUsedAt to SkillListItem - Add getRatings and addRating to skillRegistryApi - Add Rating System section to DesignGuide with all variants - Fix SkillCard fixture and DesignGuide examples to include new SkillListItem fields --- ui/src/api/skillRegistry.ts | 17 ++++++++ ui/src/components/SkillCard.test.tsx | 3 ++ ui/src/components/StarRating.tsx | 65 ++++++++++++++++++++++++++++ ui/src/pages/DesignGuide.tsx | 58 +++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 ui/src/components/StarRating.tsx diff --git a/ui/src/api/skillRegistry.ts b/ui/src/api/skillRegistry.ts index b8b0a422..ac29fc1b 100644 --- a/ui/src/api/skillRegistry.ts +++ b/ui/src/api/skillRegistry.ts @@ -10,6 +10,19 @@ export type SkillListItem = { removedAt: number | null; averageRating: number | null; ratingCount: number | null; + taskCount: number | null; + avgCostUsd: number | null; + lastUsedAt: number | null; +}; + +export type PersonalRating = { + id: string; + skillId: string; + versionId: string | null; + stars: number; + note: string | null; + createdAt: number; + updatedAt: number; }; export type SkillVersion = { @@ -43,4 +56,8 @@ export const skillRegistryApi = { api.post(`${skillPath(skillId)}/rollback`, { versionId, agentSkillsDir }), remove: (skillId: string) => api.delete(skillPath(skillId)), + getRatings: (skillId: string) => + api.get(`${skillPath(skillId)}/ratings`), + addRating: (skillId: string, body: { stars: number; versionId?: string; note?: string }) => + api.post(`${skillPath(skillId)}/ratings`, body), }; diff --git a/ui/src/components/SkillCard.test.tsx b/ui/src/components/SkillCard.test.tsx index c12ab78e..243d4f99 100644 --- a/ui/src/components/SkillCard.test.tsx +++ b/ui/src/components/SkillCard.test.tsx @@ -21,6 +21,9 @@ const mockSkill: SkillListItem = { removedAt: null, averageRating: 4.2, ratingCount: 10, + taskCount: null, + avgCostUsd: null, + lastUsedAt: null, }; describe("SkillCard", () => { diff --git a/ui/src/components/StarRating.tsx b/ui/src/components/StarRating.tsx new file mode 100644 index 00000000..1a829eaa --- /dev/null +++ b/ui/src/components/StarRating.tsx @@ -0,0 +1,65 @@ +import { Star } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +interface StarRatingProps { + value: number; + onChange?: (v: number) => void; + readonly?: boolean; + size?: "sm" | "md"; +} + +export function StarRating({ + value, + onChange, + readonly = false, + size = "md", +}: StarRatingProps) { + const iconClass = size === "sm" ? "h-3.5 w-3.5" : "h-5 w-5"; + + const stars = ( + + {[1, 2, 3, 4, 5].map((star) => { + const filled = star <= value; + return ( + + ); + })} + + ); + + if (readonly) { + return ( + + + {stars} + + {value} out of 5 stars + + ); + } + + return stars; +} diff --git a/ui/src/pages/DesignGuide.tsx b/ui/src/pages/DesignGuide.tsx index 3dc4c27f..a35cb574 100644 --- a/ui/src/pages/DesignGuide.tsx +++ b/ui/src/pages/DesignGuide.tsx @@ -127,6 +127,7 @@ import { PageSkeleton } from "@/components/PageSkeleton"; import { Identity } from "@/components/Identity"; import { SkillCard } from "@/components/SkillCard"; import { GroupBadge } from "@/components/GroupBadge"; +import { StarRating } from "@/components/StarRating"; /* ------------------------------------------------------------------ */ /* Section wrapper */ @@ -191,6 +192,7 @@ export function DesignGuide() { { key: "status", label: "Status", value: "Active" }, { key: "priority", label: "Priority", value: "High" }, ]); + const [starValue, setStarValue] = useState(3); return (
@@ -1275,6 +1277,9 @@ export function DesignGuide() { removedAt: null, averageRating: 4.7, ratingCount: 42, + taskCount: null, + avgCostUsd: null, + lastUsedAt: null, }} onInstall={() => {}} /> @@ -1290,6 +1295,9 @@ export function DesignGuide() { removedAt: null, averageRating: null, ratingCount: null, + taskCount: null, + avgCostUsd: null, + lastUsedAt: null, }} isInstalled onRollback={() => {}} @@ -1307,6 +1315,9 @@ export function DesignGuide() { removedAt: null, averageRating: 3.9, ratingCount: 15, + taskCount: null, + avgCostUsd: null, + lastUsedAt: null, }} isInstalled hasUpdate @@ -1326,6 +1337,9 @@ export function DesignGuide() { removedAt: null, averageRating: 4.2, ratingCount: 38, + taskCount: null, + avgCostUsd: null, + lastUsedAt: null, }} onInstall={() => {}} isLoading @@ -1342,6 +1356,9 @@ export function DesignGuide() { removedAt: null, averageRating: null, ratingCount: null, + taskCount: null, + avgCostUsd: null, + lastUsedAt: null, }} isInstalled hasUpdate @@ -1447,6 +1464,47 @@ export function DesignGuide() { + {/* ============================================================ */} + {/* RATING SYSTEM */} + {/* ============================================================ */} +
+ + + console.log("rated", v)} /> + + + + + + +

Current value: {starValue}

+
+ + + console.log("rated", v)} /> + + + + + + + + + +
+
+ sm + +
+
+ md + +
+
+
+
+
+ {/* ============================================================ */} {/* KEYBOARD SHORTCUTS */} {/* ============================================================ */}