From 9dd59a368f4a6afb2f2770233fcfb8cb67a661dc Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Wed, 22 Jul 2026 13:58:40 +0800 Subject: [PATCH 1/6] feat(studio): add AgentKit skill browser --- README.md | 3 + docs/content/docs/framework/frontend.en.mdx | 4 +- docs/content/docs/framework/frontend.mdx | 3 +- frontend/src/App.tsx | 6 +- frontend/src/create/skills/skillspace.ts | 46 ++ frontend/src/styles.css | 409 ++++++++++++++- frontend/src/ui/Markdown.tsx | 12 +- frontend/src/ui/SkillCenter.tsx | 472 +++++++++++++++++- frontend/tests/skillCenter.test.mjs | 105 ++++ tests/cli/test_frontend_skill_spaces.py | 328 ++++++++++++ veadk/cli/cli_frontend.py | 75 ++- ...tor-C4sQGBmQ.js => CodeEditor-BHAldtE3.js} | 2 +- ...F0.js => MarkdownPromptEditor-5LkXCkCh.js} | 2 +- .../{index-DxU-BSPC.js => index-3yhLqTTj.js} | 392 ++++++++------- veadk/webui/assets/index-DjmDWs5x.css | 10 + veadk/webui/assets/index-gxC9JS9u.css | 10 - veadk/webui/index.html | 4 +- 17 files changed, 1628 insertions(+), 255 deletions(-) create mode 100644 frontend/tests/skillCenter.test.mjs create mode 100644 tests/cli/test_frontend_skill_spaces.py rename veadk/webui/assets/{CodeEditor-C4sQGBmQ.js => CodeEditor-BHAldtE3.js} (99%) rename veadk/webui/assets/{MarkdownPromptEditor-Dd-SgWF0.js => MarkdownPromptEditor-5LkXCkCh.js} (99%) rename veadk/webui/assets/{index-DxU-BSPC.js => index-3yhLqTTj.js} (52%) create mode 100644 veadk/webui/assets/index-DjmDWs5x.css delete mode 100644 veadk/webui/assets/index-gxC9JS9u.css diff --git a/README.md b/README.md index 6d494325d..bd28cb1f7 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,9 @@ between this live metadata and control-plane information without exposing prompt or credentials. The same metadata advertises mounted smart-search sources, so Studio can disable unavailable sources up front and query the Agent's web-search tool, KnowledgeBase, or long-term memory without exposing component credentials. +When configuring skills, Studio can also browse account-scoped AgentKit Skill +Spaces and their paginated skill lists by region and project. These requests are +signed on the server, so browser clients never receive Volcengine credentials. The Studio deployment flow lists Feishu, knowledge-base, short-/long-term memory, and observability settings in their feature sections. Values entered diff --git a/docs/content/docs/framework/frontend.en.mdx b/docs/content/docs/framework/frontend.en.mdx index 47c3b6142..9db5a6da8 100644 --- a/docs/content/docs/framework/frontend.en.mdx +++ b/docs/content/docs/framework/frontend.en.mdx @@ -26,7 +26,9 @@ VeADK ships a React frontend that renders [A2UI](/en/docs/framework/a2ui) (agent - **Add an AgentKit agent**: paste a URL + API key to connect a remote agent over the ADK protocol; it then appears in the picker. - **Custom-agent workbench**: author system prompts in a rich Markdown editor with heading and list shortcuts. Local skills accept a dropped folder or ZIP - and detect the format automatically. Then debug the agent beside its configuration. + and detect the format automatically. You can also browse account-scoped + AgentKit Skill Spaces and their skills by region and project. Then debug the + agent beside its configuration. Initialization, session, and conversation failures show credential-redacted error details and Runner logs that can be expanded and copied. The deployment page shows the Agent topology on the left, with hover details, configuration diff --git a/docs/content/docs/framework/frontend.mdx b/docs/content/docs/framework/frontend.mdx index aced2e796..7652fc83b 100644 --- a/docs/content/docs/framework/frontend.mdx +++ b/docs/content/docs/framework/frontend.mdx @@ -23,7 +23,8 @@ VeADK 自带一个 React 前端,用于渲染智能体经由 Google ADK API Ser - **智能搜索**:「会话」源在当前 agent 的历史消息中做全文检索;「网页」源调用该 agent 挂载的联网搜索工具实时检索(使用服务端环境变量里的凭据)。 - **添加 AgentKit 智能体**:填入访问地址 + API Key,按 ADK 协议接入远程 agent,接入后出现在选择器中。 - **自定义 Agent 工作台**:系统提示词支持所见即所得的 Markdown - 编辑与标题、列表等快捷输入;本地技能可直接拖入文件夹或 ZIP,格式会自动识别。 + 编辑与标题、列表等快捷输入;本地技能可直接拖入文件夹或 ZIP,格式会自动识别, + 也可按地域和项目浏览账号下的 AgentKit Skill Space 及其中的技能。 完成配置后可在同页调试。调试初始化、 会话或对话失败时会展示经过凭据脱敏的具体错误和 Runner 日志, 并支持展开与复制。部署页左侧展示 Agent 拓扑,悬停可核对节点信息与配置, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 10df4b453..8410c6da8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1625,7 +1625,7 @@ export default function App() { : showAddAgent ? "添加 AgentKit 智能体" : skillCenter - ? "技能中心" + ? undefined : searchView ? "搜索" : showManageAgents @@ -1635,7 +1635,9 @@ export default function App() { : conversationTitle } crumbs={ - searchView || showAddAgent || skillCenter || showAddMenu || !visibleCreateView + skillCenter + ? [{ label: "技能中心" }, { label: "AgentKit Skill 空间" }] + : searchView || showAddAgent || showAddMenu || !visibleCreateView ? undefined : visibleCreateView === "menu" ? [ diff --git a/frontend/src/create/skills/skillspace.ts b/frontend/src/create/skills/skillspace.ts index 631fd18f0..2fad7dffb 100644 --- a/frontend/src/create/skills/skillspace.ts +++ b/frontend/src/create/skills/skillspace.ts @@ -12,6 +12,9 @@ export interface SkillSpaceRef { description: string; status: string; region?: string; + projectName?: string; + updatedAt?: string; + skillCount?: number; } export interface SkillSpaceSkill { skillId: string; @@ -31,6 +34,20 @@ export interface SkillDetail { tosPath: string; } +export interface SkillSpacePage { + items: T[]; + totalCount: number; + page: number; + pageSize: number; +} + +export interface SkillSpacePageOptions { + region: string; + page: number; + pageSize: number; + project?: string; +} + async function jfetch(url: string): Promise { const res = await fetch(url, { headers: { accept: "application/json" }, @@ -63,6 +80,18 @@ export async function listSkillSpaces(): Promise { return data.items || []; } +export async function listSkillSpacesPage( + options: SkillSpacePageOptions, +): Promise> { + const params = new URLSearchParams({ + region: options.region, + page: String(options.page), + page_size: String(options.pageSize), + }); + if (options.project) params.set("project", options.project); + return jfetch>(`/web/skill-spaces?${params.toString()}`); +} + export async function listSkillsInSpace(spaceId: string, region?: string): Promise { const q = region ? `?region=${encodeURIComponent(region)}` : ""; const data = await jfetch<{ items: SkillSpaceSkill[] }>( @@ -71,15 +100,32 @@ export async function listSkillsInSpace(spaceId: string, region?: string): Promi return data.items || []; } +export async function listSkillsInSpacePage( + spaceId: string, + options: SkillSpacePageOptions, +): Promise> { + const params = new URLSearchParams({ + region: options.region, + page: String(options.page), + page_size: String(options.pageSize), + }); + if (options.project) params.set("project", options.project); + return jfetch>( + `/web/skill-spaces/${encodeURIComponent(spaceId)}/skills?${params.toString()}`, + ); +} + export async function getSkillDetail( spaceId: string, skillId: string, version?: string, region?: string, + project?: string, ): Promise { const params: string[] = []; if (version) params.push(`version=${encodeURIComponent(version)}`); if (region) params.push(`region=${encodeURIComponent(region)}`); + if (project) params.push(`project=${encodeURIComponent(project)}`); const q = params.length > 0 ? `?${params.join("&")}` : ""; return jfetch( `/web/skill-spaces/${encodeURIComponent(spaceId)}/skills/${encodeURIComponent(skillId)}${q}`, diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 238a14845..e78abecda 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -3341,12 +3341,413 @@ body { right: 10px; } -/* ---------- skill center (external iframe filling the main panel) ---------- */ -.skill-frame { +/* ---------- skill center ---------- */ +.skillcenter { flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + padding: 0; +} +.skillcenter-regions { + display: grid; + padding: 2px; + border: 1px solid hsl(var(--border)); + background: hsl(var(--canvas) / 0.62); +} +.skillcenter-regions button { + border: 0; + background: transparent; + color: hsl(var(--muted-foreground)); + font: inherit; + cursor: pointer; +} +.skillcenter-regions button.active { + background: hsl(var(--background)); + color: hsl(var(--foreground)); + box-shadow: 0 1px 2px hsl(var(--foreground) / 0.07); +} +.skillcenter-regions button:focus-visible, +.skillcenter-pager button:focus-visible, +.skill-detail-close:focus-visible { + outline: 2px solid hsl(var(--ring) / 0.28); + outline-offset: 1px; +} +.skillcenter-space-item:focus-visible, +.skillcenter-skill-item:focus-visible { + outline: none; + border-color: transparent; + background: hsl(var(--muted) / 0.62); +} +.skillcenter-regions { + grid-template-columns: repeat(2, 58px); + border-radius: 7px; +} +.skillcenter-regions button { + height: 27px; + border-radius: 5px; + font-size: 11.5px; +} +.skillcenter-browser { + flex: 1; + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: minmax(270px, 0.9fr) minmax(360px, 1.35fr); + gap: 0; +} +.skillcenter-panel { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} +.skillcenter-panel + .skillcenter-panel { + border-left: 1px solid hsl(var(--border)); +} +.skillcenter-panel-head { + height: 48px; + flex: 0 0 48px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 14px; + border-bottom: 1px solid hsl(var(--border)); +} +.skillcenter-panel-head > div { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; +} +.skillcenter-panel-head .icon { + width: 17px; + height: 17px; + color: hsl(var(--muted-foreground)); +} +.skillcenter-panel-head h2 { + min-width: 0; + margin: 0; + overflow: hidden; + color: hsl(var(--foreground)); + font-size: 13.5px; + font-weight: 620; + text-overflow: ellipsis; + white-space: nowrap; +} +.skillcenter-panel-head > span { + flex-shrink: 0; + color: hsl(var(--muted-foreground)); + font-size: 11.5px; +} +.skillcenter-count-badge { + min-width: 25px; + height: 21px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 7px; + border-radius: 999px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + font-size: 11px; + font-weight: 600; + line-height: 1; +} +.skillcenter-listwrap { + position: relative; + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} +.skillcenter-list { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px; +} +.skillcenter-space-item, +.skillcenter-skill-item { width: 100%; - border: none; - background: hsl(var(--panel)); + min-width: 0; + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: hsl(var(--foreground)); + font: inherit; + text-align: left; + cursor: pointer; + transition: background-color 120ms ease, border-color 120ms ease; +} +.skillcenter-space-item:hover, +.skillcenter-skill-item:hover { + border-color: transparent; + background: hsl(var(--muted) / 0.62); +} +.skillcenter-space-item.active { + border-color: transparent; + background: hsl(var(--muted) / 0.62); +} +.skillcenter-symbol { + width: 30px; + height: 30px; + flex: 0 0 30px; + display: grid; + place-items: center; + border: 1px solid hsl(var(--border)); + border-radius: 7px; + background: hsl(var(--background)); + color: hsl(var(--foreground) / 0.78); +} +.skillcenter-symbol .icon { width: 18px; height: 18px; } +.skillcenter-symbol--skill { + color: hsl(214 64% 43%); + background: hsl(214 75% 97%); + border-color: hsl(214 54% 87%); +} +.skillcenter-item-body { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} +.skillcenter-item-title { + min-width: 0; + overflow: hidden; + font-size: 13px; + font-weight: 600; + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; +} +.skillcenter-item-description { + display: -webkit-box; + min-width: 0; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 17px; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} +.skillcenter-item-meta { + min-width: 0; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 5px 8px; + margin-top: 2px; +} +.skillcenter-status { + flex-shrink: 0; + padding: 2px 6px; + border-radius: 999px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 16px; +} +.skillcenter-status.is-positive { color: hsl(145 61% 29%); background: hsl(145 55% 94%); } +.skillcenter-status.is-progress { color: hsl(35 78% 31%); background: hsl(42 86% 94%); } +.skillcenter-status.is-danger { color: hsl(var(--destructive)); background: hsl(var(--destructive) / 0.09); } +.skillcenter-meta-text { + min-width: 0; + max-width: 170px; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 10.5px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} +.skillcenter-pager { + height: 44px; + flex: 0 0 44px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 12px; + border-top: 1px solid hsl(var(--border)); + color: hsl(var(--muted-foreground)); + font-size: 11.5px; +} +.skillcenter-pager-actions { display: flex; align-items: center; gap: 7px; } +.skillcenter-pager-actions > span { min-width: 38px; text-align: center; } +.skillcenter-pager button, +.skill-detail-close { + display: grid; + place-items: center; + border: 0; + border-radius: 6px; + background: transparent; + color: hsl(var(--muted-foreground)); + cursor: pointer; +} +.skillcenter-pager button { width: 26px; height: 26px; padding: 0; } +.skillcenter-pager button:hover:not(:disabled), +.skill-detail-close:hover { color: hsl(var(--foreground)); background: hsl(var(--accent)); } +.skillcenter-pager button:disabled { opacity: 0.35; cursor: default; } +.skillcenter-pager button .icon { width: 17px; height: 17px; } +.skillcenter-empty, +.skillcenter-loading, +.skillcenter-error { + min-height: 130px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px; + color: hsl(var(--muted-foreground)); + font-size: 12.5px; + line-height: 1.55; + text-align: center; + overflow-wrap: anywhere; +} +.skillcenter-error { color: hsl(var(--destructive)); } +.skillcenter-loading--overlay { + position: absolute; + inset: 0; + z-index: 2; + min-height: 0; + background: hsl(var(--background) / 0.82); + backdrop-filter: blur(1px); +} +.skillcenter-loading-mark { + width: 14px; + height: 14px; + flex-shrink: 0; + border: 1.5px solid hsl(var(--foreground) / 0.16); + border-top-color: hsl(var(--foreground) / 0.62); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +.skill-detail-backdrop { + position: fixed; + inset: 0; + z-index: 80; + display: grid; + place-items: center; + padding: 16px; + background: hsl(var(--foreground) / 0.25); + backdrop-filter: blur(3px); +} +.skill-detail-dialog { + width: min(760px, 100%); + height: min(760px, 100%); + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid hsl(var(--border)); + border-radius: 12px; + background: hsl(var(--background)); + box-shadow: 0 18px 48px hsl(var(--foreground) / 0.16); +} +.skill-detail-head { + flex-shrink: 0; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 16px 18px 14px; + border-bottom: 1px solid hsl(var(--border)); +} +.skill-detail-heading { min-width: 0; display: flex; align-items: flex-start; gap: 11px; } +.skill-detail-heading > div { min-width: 0; } +.skill-detail-heading h2 { + margin: 1px 0 4px; + overflow: hidden; + font-size: 16px; + font-weight: 650; + line-height: 22px; + text-overflow: ellipsis; + white-space: nowrap; +} +.skill-detail-heading p { + display: -webkit-box; + margin: 0; + overflow: hidden; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 17px; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} +.skill-detail-close { width: 30px; height: 30px; flex: 0 0 30px; padding: 0; } +.skill-detail-meta { + flex-shrink: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px 18px; + margin: 0; + padding: 14px 18px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--canvas) / 0.45); +} +.skill-detail-meta > div { min-width: 0; } +.skill-detail-meta dt { margin-bottom: 3px; color: hsl(var(--muted-foreground)); font-size: 10.5px; } +.skill-detail-meta dd { + margin: 0; + overflow: hidden; + color: hsl(var(--foreground)); + font-size: 12px; + line-height: 17px; + text-overflow: ellipsis; + white-space: nowrap; +} +.skill-detail-content { flex: 1; min-height: 0; display: flex; flex-direction: column; } +.skill-detail-content-title { + height: 40px; + flex: 0 0 40px; + display: flex; + align-items: center; + padding: 0 18px; + border-bottom: 1px solid hsl(var(--border)); + font-size: 12px; + font-weight: 600; +} +.skill-detail-content > .skillcenter-loading, +.skill-detail-content > .skillcenter-error, +.skill-detail-content > .skillcenter-empty { flex: 1; min-height: 0; } +.skill-detail-markdown { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 18px 22px 28px; + overflow-wrap: anywhere; +} + +@media (max-width: 760px) { + .skillcenter-browser { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: repeat(2, minmax(0, 1fr)); + } + .skillcenter-panel + .skillcenter-panel { + border-top: 1px solid hsl(var(--border)); + border-left: 0; + } + .skill-detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 560px) { + .skillcenter-regions { grid-template-columns: repeat(2, 46px); } + .skillcenter-browser { gap: 8px; } + .skillcenter-item-meta { gap: 4px 6px; } + .skillcenter-meta-text { max-width: 132px; } + .skill-detail-backdrop { padding: 8px; } + .skill-detail-dialog { border-radius: 10px; } + .skill-detail-meta { grid-template-columns: minmax(0, 1fr); gap: 8px; max-height: 180px; overflow-y: auto; } } /* ---------- add AgentKit agent (URL + key form in the main panel) ---------- */ diff --git a/frontend/src/ui/Markdown.tsx b/frontend/src/ui/Markdown.tsx index feb04b359..323409ae0 100644 --- a/frontend/src/ui/Markdown.tsx +++ b/frontend/src/ui/Markdown.tsx @@ -49,7 +49,15 @@ function isVideoLink(node: any): boolean { * * Streaming-safe: re-renders cleanly as `text` grows. Links open in a new * tab. Memoized so unrelated turn re-renders don't re-parse the tree. */ -function MarkdownImpl({ text, className }: { text: string; className?: string }) { +function MarkdownImpl({ + text, + className, + allowRawHtml = true, +}: { + text: string; + className?: string; + allowRawHtml?: boolean; +}) { const [videoViewerOpen, setVideoViewerOpen] = useState(null); // Extract video src from props or source children @@ -101,7 +109,7 @@ function MarkdownImpl({ text, className }: { text: string; className?: string })
{ const href = props.href; diff --git a/frontend/src/ui/SkillCenter.tsx b/frontend/src/ui/SkillCenter.tsx index a5fec68c4..628e17aff 100644 --- a/frontend/src/ui/SkillCenter.tsx +++ b/frontend/src/ui/SkillCenter.tsx @@ -1,39 +1,467 @@ -const SKILL_URL = "https://findskill.com/"; - -/** Hand-drawn "skill center" mark: a compass rose (discover skills) in a ring. */ -function SkillCenterIcon() { - return ( - - +import { useEffect, useRef, useState } from "react"; +import { + getSkillDetail, + listSkillSpacesPage, + listSkillsInSpacePage, + type SkillDetail, + type SkillSpaceRef, + type SkillSpaceSkill, +} from "../create/skills/skillspace"; +import { Markdown } from "./Markdown"; + +const SPACE_PAGE_SIZE = 6; +const SKILL_PAGE_SIZE = 7; + +type SkillRegion = "cn-beijing" | "cn-shanghai"; + +const STATUS_LABELS: Record = { + active: "可用", + available: "可用", + creating: "创建中", + disabled: "已停用", + enabled: "已启用", + failed: "异常", + inactive: "未启用", + pending: "等待中", + published: "已发布", + ready: "就绪", + released: "已发布", + running: "运行中", + success: "正常", + unavailable: "不可用", + unreleased: "未发布", + updating: "更新中", +}; + +function statusLabel(status?: string): string { + return STATUS_LABELS[(status || "").trim().toLowerCase()] || "未知"; +} + +function statusTone(status?: string): string { + const value = (status || "").toLowerCase(); + if (["active", "available", "enabled", "published", "ready", "released", "success"].includes(value)) { + return "is-positive"; + } + if (["creating", "pending", "running", "updating"].includes(value)) return "is-progress"; + if (["failed", "unavailable"].includes(value)) return "is-danger"; + return "is-muted"; +} + +function updatedAtLabel(value?: string): string { + if (!value) return ""; + const trimmed = value.trim(); + const numeric = Number(trimmed); + const date = /^\d+(?:\.\d+)?$/.test(trimmed) + ? new Date(numeric < 1_000_000_000_000 ? numeric * 1000 : numeric) + : new Date(trimmed); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).format(date); +} + +function skillMarkdownBody(value: string): string { + const normalized = value.replace(/\r\n/g, "\n"); + if (!normalized.startsWith("---\n")) return value; + const closingDelimiter = normalized.indexOf("\n---\n", 4); + return closingDelimiter >= 0 + ? normalized.slice(closingDelimiter + 5).trimStart() + : value; +} + +/** Hand-drawn Skill Space mark: two connected shelves for a skill collection. */ +function SkillSpaceIcon({ className = "icon" }: { className?: string }) { + return ( + + + + + + ); +} + +/** Hand-drawn Skill mark: a compact instruction card with an activation spark. */ +function SkillIcon({ className = "icon" }: { className?: string }) { + return ( + + + + + + ); +} + +function CloseIcon() { + return ( + + + + ); +} + +function ArrowIcon({ direction }: { direction: "left" | "right" }) { + return ( + ); } +function LoadingMark() { + return ; +} + +function Pager({ + page, + total, + pageSize, + onPage, +}: { + page: number; + total: number; + pageSize: number; + onPage: (page: number) => void; +}) { + const pageCount = Math.max(1, Math.ceil(total / pageSize)); + return ( +
+ 共 {total} 项 +
+ + {page} / {pageCount} + +
+
+ ); +} + +function EmptyState({ children }: { children: string }) { + return
{children}
; +} + +function SkillDetailDialog({ + skill, + space, + region, + detail, + loading, + error, + onClose, +}: { + skill: SkillSpaceSkill; + space: SkillSpaceRef; + region: SkillRegion; + detail: SkillDetail | null; + loading: boolean; + error: string; + onClose: () => void; +}) { + useEffect(() => { + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") onClose(); + }; + window.addEventListener("keydown", closeOnEscape); + return () => window.removeEventListener("keydown", closeOnEscape); + }, [onClose]); + + return ( +
+
event.stopPropagation()} + > +
+
+ +
+

{detail?.name || skill.skillName}

+

{detail?.description || skill.skillDescription || "暂无描述"}

+
+
+ +
+ +
+
技能 ID
{skill.skillId}
+
版本
{detail?.version || skill.version || "—"}
+
状态
{statusLabel(skill.skillStatus)}
+
技能空间
{space.name}
+
Project
{space.projectName || "default"}
+
地域
{region === "cn-beijing" ? "北京" : "上海"}
+
+ +
+
SKILL.md
+ {loading ? ( +
正在读取技能内容…
+ ) : error ? ( +
{error}
+ ) : detail?.skillMd ? ( + + ) : ( + 该技能暂无 SKILL.md 内容 + )} +
+
+
+ ); +} + /** Sidebar entry that opens the skill center view in the main panel. */ export function SkillCenterButton({ onClick }: { onClick: () => void }) { return ( ); } -/** Full-panel embed of the external skill center. */ +/** Native AgentKit Skill space browser. */ export function SkillCenterView() { - return