// Marketplace app —— PR-S.1c rewrite per Claude Design bundle 8Uv-tmaD1W9VPBLDMeqJhg.
//
// Layout(完全照设计稿):
// topbar:brand · tabs(Playground/Manage/Evals/Traces/[Marketplace active])· Publish · My installs
// market-tabs:Skills | MCP Servers(各自 count badge)
// market-toolbar:search + (cats — 仅当后端给了 category 才渲染) + installed counter + sort
// market-scroll → market-grid(4 / row,48 / page)→ Pager
//
// Card(简化版,跟设计稿最后一轮一致):
// icon(soft-tinted initials,左)+ body(name + 2-line desc + minimal stats)+ add-btn(右)
// stats 我们用真后端字段而不是 fake installs / rating:
// - skill:`v{latest_version}` + `verified ✓`(visibility=public)
// - mcp:`{transport}` + `available ✓` / `unavailable ⚠`
//
// 数据(并发拉 4 路,跟 PR-S.1b 一致):
// GET /v1/marketplace/skills — 公共 catalog,不含 per-user 字段
// GET /v1/marketplace/mcps — 同上
// GET /v1/skills — per-user 视角(看 `enabled` 判断 installed)
// GET /v1/user/bindings — 真相源:skill_ids ∪ mcp_ids
//
// Install / Uninstall(行为不变,跟 PR-S.1b 同):
// skill install → POST /v1/user/skills/{id}/install
// skill uninstall:
// · owner_user_id == 我 → DELETE /v1/personal-skills/{id}(物理删 clone)
// · 其它 → PATCH /v1/user/bindings 把 id 从 skill_ids 摘掉
// mcp install → PATCH /v1/user/bindings 把 server_id append 到 mcp_ids
// mcp uninstall → PATCH /v1/user/bindings 把 server_id 从 mcp_ids 摘掉
//
// 任何 mutation 后 refreshUserState() 重拉 listSkills + getUserBindings 再算 installed 集合。
const { useState: mkUseState, useEffect: mkUseEffect, useMemo: mkUseMemo, useCallback: mkUseCallback } = React;
const PAGE_SIZE = 48; // 4 per row × 12 rows
// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------
// Map a hex like "#2563eb" to a soft pastel bg by adding alpha. CSS file's
// `.app-card .icon` paints text in `item.color`; we paint bg as `color + "1f"`
// (12% alpha) here, matching the design's pastel tint.
function tintBg(color) {
if (!color || typeof color !== "string" || !color.startsWith("#")) {
return "rgba(13, 13, 13, 0.06)";
}
return color + "1f";
}
// Distinct categories present in this list. Backend doesn't yet ship a
// `category` field; for V1 (no categories) we return ["All"] and the chips
// bar is skipped entirely by the caller.
function deriveCategories(items) {
const set = new Set();
for (const it of items) {
if (it && typeof it.category === "string" && it.category) set.add(it.category);
}
return ["All", ...Array.from(set).sort()];
}
// ----------------------------------------------------------------------------
// AppCard — icon + body (name / desc / minimal stats) + + button
// ----------------------------------------------------------------------------
function AppCard({ item, kind, installed, busy, onInstall, onUninstall, disabledReason }) {
const handle = () => {
if (busy) return;
if (disabledReason && !installed) return;
if (installed) onUninstall();
else onInstall();
};
// Stats line — real backend fields, not fake installs/rating.
let statsBits;
if (kind === "skill") {
statsBits = (
<>
{item.latest_version && v{item.latest_version}}
{item.visibility === "public" && (
Installing will shadow a same-name skill that already belongs to:
When you run that agent, your installed version takes priority (user > agent on same-name).
> )}Fetching marketplace catalog and your installs.
{installedOnly ? `You haven't installed any ${tab === "skill" ? "skills" : "MCP servers"} yet. Browse the catalog and click + to install.` : (query || cat !== "All") ? "Try a different search term or category." : "Admins can add catalog items from the Manage page."}
{installedOnly && ()}