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": "无法预览",