dify/web/app/components/workflow/skill/start-tab/skill-templates-section.tsx
yyh 038b03fa8e
feat(skill): add script-driven full skill template generation
Add fetch-skill-templates.ts script that clones anthropics/skills repo
and generates complete directory trees (scripts, references, assets)
for all 16 skills with base64 encoding for binary files, replacing
the previous single-SKILL.md-only approach. Generated files are
lazy-loaded per skill on user click.
2026-01-30 16:10:18 +08:00

106 lines
3.4 KiB
TypeScript

'use client'
import type { SkillTemplateSummary } from './templates/types'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useBatchUpload } from '@/service/use-app-asset'
import { useSkillTreeUpdateEmitter } from '../hooks/use-skill-tree-collaboration'
import CategoryTabs from './category-tabs'
import SectionHeader from './section-header'
import TemplateCard from './template-card'
import TemplateSearch from './template-search'
import { SKILL_TEMPLATES } from './templates/registry'
import { buildUploadDataFromTemplate } from './templates/template-to-upload'
const SkillTemplatesSection = () => {
const { t } = useTranslation('workflow')
const [activeCategory, setActiveCategory] = useState('all')
const [searchValue, setSearchValue] = useState('')
const [loadingId, setLoadingId] = useState<string | null>(null)
const appDetail = useAppStore(s => s.appDetail)
const appId = appDetail?.id || ''
const storeApi = useWorkflowStore()
const batchUpload = useBatchUpload()
const emitTreeUpdate = useSkillTreeUpdateEmitter()
const handleUse = useCallback(async (summary: SkillTemplateSummary) => {
const entry = SKILL_TEMPLATES.find(e => e.id === summary.id)
if (!entry || !appId)
return
setLoadingId(summary.id)
storeApi.getState().setUploadStatus('uploading')
storeApi.getState().setUploadProgress({ uploaded: 0, total: 1, failed: 0 })
try {
const children = await entry.loadContent()
const uploadData = await buildUploadDataFromTemplate(summary.name, children)
await batchUpload.mutateAsync({
appId,
tree: uploadData.tree,
files: uploadData.files,
parentId: null,
onProgress: (uploaded, total) => {
storeApi.getState().setUploadProgress({ uploaded, total, failed: 0 })
},
})
storeApi.getState().setUploadStatus('success')
emitTreeUpdate()
}
catch {
storeApi.getState().setUploadStatus('partial_error')
}
finally {
setLoadingId(null)
}
}, [appId, batchUpload, storeApi, emitTreeUpdate])
const filtered = SKILL_TEMPLATES.filter((entry) => {
if (searchValue) {
const q = searchValue.toLowerCase()
return entry.name.toLowerCase().includes(q) || entry.description.toLowerCase().includes(q)
}
if (activeCategory !== 'all')
return entry.tags?.some(tag => tag.toLowerCase() === activeCategory.toLowerCase())
return true
})
return (
<section className="flex flex-col gap-3 px-6 py-2">
<SectionHeader
title={t('skill.startTab.templatesTitle')}
description={t('skill.startTab.templatesDesc')}
/>
<div className="flex w-full items-start gap-1">
<CategoryTabs
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<TemplateSearch
value={searchValue}
onChange={setSearchValue}
/>
</div>
<div className="grid grid-cols-3 gap-3">
{filtered.map(entry => (
<TemplateCard
key={entry.id}
template={entry}
onUse={handleUse}
/>
))}
</div>
{loadingId && (
<div className="pointer-events-none fixed inset-0 z-50" />
)}
</section>
)
}
export default memo(SkillTemplatesSection)