feat(skill): add skill template types, card component and upload utility

Introduce type definitions separating raw skill data (SkillTemplate)
from UI metadata (SkillTemplateWithMetadata) to match the actual
skill format from upstream repos. Add template card component with
hover state and file count display, template-to-upload conversion
utility, and i18n keys for en-US/zh-Hans.
This commit is contained in:
yyh
2026-01-30 14:01:32 +08:00
parent f5b84384cf
commit 66b4fa102b
5 changed files with 162 additions and 0 deletions

View File

@ -0,0 +1,76 @@
'use client'
import type { SkillTemplateNode, SkillTemplateWithMetadata } from './templates/types'
import { RiAddLine } from '@remixicon/react'
import { memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
function countFiles(nodes: SkillTemplateNode[]): number {
return nodes.reduce((count, node) => {
if (node.node_type === 'file')
return count + 1
return count + countFiles(node.children)
}, 0)
}
type TemplateCardProps = {
template: SkillTemplateWithMetadata
onUse: (template: SkillTemplateWithMetadata) => void
}
const TemplateCard = ({ template, onUse }: TemplateCardProps) => {
const { t } = useTranslation('workflow')
const fileCount = useMemo(() => countFiles(template.children), [template.children])
return (
<div className="group flex h-full flex-col overflow-hidden rounded-xl border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg transition-colors hover:bg-components-panel-on-panel-item-bg-hover">
<div className="flex items-center gap-3 px-4 pb-2 pt-4">
<AppIcon
size="large"
icon={template.icon || '📁'}
background="#f5f3ff"
/>
<div className="flex min-w-0 flex-1 flex-col gap-0.5 py-px">
<span className="system-md-semibold truncate text-text-secondary">
{template.name}
</span>
<span className="system-xs-regular text-text-tertiary">
{t('skill.startTab.filesIncluded', { count: fileCount })}
</span>
</div>
</div>
<div className="flex flex-1 flex-col px-4 py-1">
<p className="system-xs-regular line-clamp-2 min-h-[32px] w-full text-text-tertiary">
{template.description}
</p>
</div>
<div className="relative px-4 pb-4">
{template.tags?.length
? (
<div className="flex flex-wrap gap-1 transition-opacity group-hover:opacity-0">
{template.tags.map(tag => (
<Badge key={tag} text={tag} />
))}
</div>
)
: <div className="h-5" />}
<div className="pointer-events-none absolute inset-0 flex items-end px-4 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
<Button
variant="primary"
size="medium"
className="w-full"
onClick={() => onUse(template)}
>
<RiAddLine className="mr-0.5 h-4 w-4" />
{t('skill.startTab.useThisSkill')}
</Button>
</div>
</div>
</div>
)
}
export default memo(TemplateCard)

View File

@ -0,0 +1,43 @@
import type { SkillTemplate, SkillTemplateNode } from './types'
import type { BatchUploadNodeInput } from '@/types/app-asset'
import { prepareSkillUploadFile } from '../../utils/skill-upload-utils'
type TemplateUploadData = {
tree: BatchUploadNodeInput[]
files: Map<string, File>
}
export async function buildUploadDataFromTemplate(
template: SkillTemplate,
): Promise<TemplateUploadData> {
const files = new Map<string, File>()
async function convertNode(
node: SkillTemplateNode,
pathPrefix: string,
): Promise<BatchUploadNodeInput> {
const currentPath = pathPrefix ? `${pathPrefix}/${node.name}` : node.name
if (node.node_type === 'folder') {
const children = await Promise.all(
node.children.map(child => convertNode(child, currentPath)),
)
return { name: node.name, node_type: 'folder', children }
}
const raw = new File([node.content], node.name, { type: 'text/plain' })
const prepared = await prepareSkillUploadFile(raw)
files.set(currentPath, prepared)
return { name: node.name, node_type: 'file', size: prepared.size }
}
const rootFolder: BatchUploadNodeInput = {
name: template.name,
node_type: 'folder',
children: await Promise.all(
template.children.map(child => convertNode(child, template.name)),
),
}
return { tree: [rootFolder], files }
}

View File

@ -0,0 +1,39 @@
import type { AssetNodeType } from '@/types/app-asset'
export type SkillTemplateFileNode = {
name: string
node_type: Extract<AssetNodeType, 'file'>
content: string
}
export type SkillTemplateFolderNode = {
name: string
node_type: Extract<AssetNodeType, 'folder'>
children: SkillTemplateNode[]
}
export type SkillTemplateNode = SkillTemplateFileNode | SkillTemplateFolderNode
export type SkillTemplateFrontmatter = {
name: string
description: string
}
export type SkillTemplate = {
id: string
name: string
description: string
children: SkillTemplateNode[]
}
export type SkillTemplateMetadata = {
tags?: string[]
icon?: string
}
export type SkillTemplateWithMetadata = SkillTemplate & SkillTemplateMetadata
export type SkillTemplateTag = {
id: string
label: string
}

View File

@ -1074,12 +1074,14 @@
"singleRun.testRunLoop": "Test Run Loop",
"skill.startTab.createBlankSkill": "Create Blank Skill",
"skill.startTab.createBlankSkillDesc": "Start with an empty folder structure",
"skill.startTab.filesIncluded": "{{count}} files included",
"skill.startTab.importSkill": "Import Skill",
"skill.startTab.importSkillDesc": "Import skill from skill.zip file",
"skill.startTab.searchPlaceholder": "Search…",
"skill.startTab.templatesComingSoon": "Templates coming soon…",
"skill.startTab.templatesDesc": "Choose a template to bootstrap your agent's capabilities",
"skill.startTab.templatesTitle": "Skill Templates",
"skill.startTab.useThisSkill": "Use this Skill",
"skillEditor.authorizationBadge": "Auth",
"skillEditor.authorizationRequired": "Authorization required before use.",
"skillEditor.previewUnavailable": "Preview unavailable",

View File

@ -1066,12 +1066,14 @@
"singleRun.testRunLoop": "测试运行循环",
"skill.startTab.createBlankSkill": "创建空白 Skill",
"skill.startTab.createBlankSkillDesc": "从空文件夹结构开始",
"skill.startTab.filesIncluded": "包含 {{count}} 个文件",
"skill.startTab.importSkill": "导入 Skill",
"skill.startTab.importSkillDesc": "从 skill.zip 文件导入",
"skill.startTab.searchPlaceholder": "搜索…",
"skill.startTab.templatesComingSoon": "模板即将推出…",
"skill.startTab.templatesDesc": "选择模板来快速构建你的 Agent 能力",
"skill.startTab.templatesTitle": "Skill 模板",
"skill.startTab.useThisSkill": "使用此 Skill",
"skillEditor.authorizationBadge": "Auth",
"skillEditor.authorizationRequired": "使用前需要授权。",
"skillEditor.previewUnavailable": "无法预览",