From 66b4fa102b8294f29d90df61ccc8e04efadc1be5 Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 30 Jan 2026 14:01:32 +0800 Subject: [PATCH] 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. --- .../skill/start-tab/template-card.tsx | 76 +++++++++++++++++++ .../start-tab/templates/template-to-upload.ts | 43 +++++++++++ .../skill/start-tab/templates/types.ts | 39 ++++++++++ web/i18n/en-US/workflow.json | 2 + web/i18n/zh-Hans/workflow.json | 2 + 5 files changed, 162 insertions(+) create mode 100644 web/app/components/workflow/skill/start-tab/template-card.tsx create mode 100644 web/app/components/workflow/skill/start-tab/templates/template-to-upload.ts create mode 100644 web/app/components/workflow/skill/start-tab/templates/types.ts diff --git a/web/app/components/workflow/skill/start-tab/template-card.tsx b/web/app/components/workflow/skill/start-tab/template-card.tsx new file mode 100644 index 0000000000..e6fe7a5718 --- /dev/null +++ b/web/app/components/workflow/skill/start-tab/template-card.tsx @@ -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 ( +
+
+ +
+ + {template.name} + + + {t('skill.startTab.filesIncluded', { count: fileCount })} + +
+
+
+

+ {template.description} +

+
+
+ {template.tags?.length + ? ( +
+ {template.tags.map(tag => ( + + ))} +
+ ) + :
} +
+ +
+
+
+ ) +} + +export default memo(TemplateCard) diff --git a/web/app/components/workflow/skill/start-tab/templates/template-to-upload.ts b/web/app/components/workflow/skill/start-tab/templates/template-to-upload.ts new file mode 100644 index 0000000000..bd6c40c36b --- /dev/null +++ b/web/app/components/workflow/skill/start-tab/templates/template-to-upload.ts @@ -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 +} + +export async function buildUploadDataFromTemplate( + template: SkillTemplate, +): Promise { + const files = new Map() + + async function convertNode( + node: SkillTemplateNode, + pathPrefix: string, + ): Promise { + 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 } +} diff --git a/web/app/components/workflow/skill/start-tab/templates/types.ts b/web/app/components/workflow/skill/start-tab/templates/types.ts new file mode 100644 index 0000000000..3e8135076c --- /dev/null +++ b/web/app/components/workflow/skill/start-tab/templates/types.ts @@ -0,0 +1,39 @@ +import type { AssetNodeType } from '@/types/app-asset' + +export type SkillTemplateFileNode = { + name: string + node_type: Extract + content: string +} + +export type SkillTemplateFolderNode = { + name: string + node_type: Extract + 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 +} diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 17b3ad90ac..0ba19d86db 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -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", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 8f6e5794cb..0b2f03ccaa 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -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": "无法预览",