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