mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 06:58:05 +08:00
Frontend: - Migrate deprecated imports: modal→dialog, toast→ui/toast, tooltip→tooltip-plus, portal-to-follow-elem→portal-to-follow-elem-plus, select→ui/select, confirm→alert-dialog - Replace next/* with @/next/* wrapper modules - Convert TypeScript enums to const objects (erasable-syntax-only) - Replace all `any` types with `unknown` or specific types in workflow types - Fix unused vars, react-hooks-extra, react-refresh/only-export-components - Extract InteractionMode to separate module, tool-block commands to commands.ts Backend: - Fix pyrefly errors: type narrowing, null guards, getattr patterns - Remove unused TYPE_CHECKING imports in LLM node - Add ignore_imports entries to .importlinter for dify_graph boundary violations Made-with: Cursor
177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
'use client'
|
|
|
|
import type { BatchUploadNodeInput } from '@/types/app-asset'
|
|
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
|
import Button from '@/app/components/base/button'
|
|
import Input from '@/app/components/base/input'
|
|
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
|
import { toast } from '@/app/components/base/ui/toast'
|
|
import { useWorkflowStore } from '@/app/components/workflow/store'
|
|
import { useBatchUpload } from '@/service/use-app-asset'
|
|
import { useExistingSkillNames } from '../hooks/file-tree/data/use-skill-asset-tree'
|
|
import { useSkillTreeUpdateEmitter } from '../hooks/file-tree/data/use-skill-tree-collaboration'
|
|
import { prepareSkillUploadFile } from '../utils/skill-upload-utils'
|
|
|
|
const SKILL_MD_TEMPLATE = (name: string) => `---
|
|
name: ${name}
|
|
description:
|
|
---
|
|
|
|
# ${name}
|
|
`
|
|
|
|
type CreateBlankSkillModalProps = {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
const CreateBlankSkillModal = ({ isOpen, onClose }: CreateBlankSkillModalProps) => {
|
|
const { t } = useTranslation()
|
|
const [skillName, setSkillName] = useState('')
|
|
const [isCreating, setIsCreating] = useState(false)
|
|
|
|
const appDetail = useAppStore(s => s.appDetail)
|
|
const appId = appDetail?.id || ''
|
|
const storeApi = useWorkflowStore()
|
|
|
|
const batchUpload = useBatchUpload()
|
|
const batchUploadRef = useRef(batchUpload)
|
|
batchUploadRef.current = batchUpload
|
|
|
|
const emitTreeUpdate = useSkillTreeUpdateEmitter()
|
|
const emitTreeUpdateRef = useRef(emitTreeUpdate)
|
|
emitTreeUpdateRef.current = emitTreeUpdate
|
|
|
|
const { data: existingNames } = useExistingSkillNames()
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (isOpen)
|
|
queueMicrotask(() => inputRef.current?.focus())
|
|
}, [isOpen])
|
|
|
|
const trimmedName = skillName.trim()
|
|
const isDuplicate = !!trimmedName && (existingNames?.has(trimmedName) ?? false)
|
|
const canCreate = !!trimmedName && !isDuplicate && !isCreating
|
|
|
|
const handleClose = useCallback(() => {
|
|
if (isCreating)
|
|
return
|
|
setSkillName('')
|
|
onClose()
|
|
}, [isCreating, onClose])
|
|
|
|
const handleCreate = useCallback(async () => {
|
|
if (!canCreate || !appId)
|
|
return
|
|
|
|
setIsCreating(true)
|
|
storeApi.getState().setUploadStatus('uploading')
|
|
storeApi.getState().setUploadProgress({ uploaded: 0, total: 1, failed: 0 })
|
|
|
|
try {
|
|
const content = SKILL_MD_TEMPLATE(trimmedName)
|
|
const rawFile = new File([content], 'SKILL.md', { type: 'text/markdown' })
|
|
const preparedFile = await prepareSkillUploadFile(rawFile)
|
|
|
|
const tree: BatchUploadNodeInput[] = [{
|
|
name: trimmedName,
|
|
node_type: 'folder',
|
|
children: [{ name: 'SKILL.md', node_type: 'file', size: preparedFile.size }],
|
|
}]
|
|
|
|
const files = new Map<string, File>()
|
|
files.set(`${trimmedName}/SKILL.md`, preparedFile)
|
|
|
|
const createdNodes = await batchUploadRef.current.mutateAsync({
|
|
appId,
|
|
tree,
|
|
files,
|
|
parentId: null,
|
|
onProgress: (uploaded, total) => {
|
|
storeApi.getState().setUploadProgress({ uploaded, total, failed: 0 })
|
|
},
|
|
})
|
|
|
|
storeApi.getState().setUploadStatus('success')
|
|
emitTreeUpdateRef.current()
|
|
|
|
const skillMdId = createdNodes?.[0]?.children?.[0]?.id
|
|
if (skillMdId)
|
|
storeApi.getState().openTab(skillMdId, { pinned: true })
|
|
|
|
toast.success(t('skill.startTab.createSuccess', { ns: 'workflow', name: trimmedName }))
|
|
onClose()
|
|
}
|
|
catch {
|
|
storeApi.getState().setUploadStatus('partial_error')
|
|
toast.error(t('skill.startTab.createError', { ns: 'workflow' }))
|
|
}
|
|
finally {
|
|
setIsCreating(false)
|
|
setSkillName('')
|
|
}
|
|
}, [canCreate, appId, trimmedName, storeApi, onClose, t])
|
|
|
|
return (
|
|
<Dialog
|
|
open={isOpen}
|
|
onOpenChange={(open) => {
|
|
if (!open)
|
|
handleClose()
|
|
}}
|
|
disablePointerDismissal={isCreating}
|
|
>
|
|
<DialogContent>
|
|
{!isCreating && <DialogCloseButton />}
|
|
<DialogTitle className="text-text-primary title-2xl-semi-bold">
|
|
{t('skill.startTab.createModal.title', { ns: 'workflow' })}
|
|
</DialogTitle>
|
|
<div className="mt-6 flex flex-col gap-1">
|
|
<label className="text-text-secondary system-sm-semibold">
|
|
{t('skill.startTab.createModal.nameLabel', { ns: 'workflow' })}
|
|
</label>
|
|
<Input
|
|
ref={inputRef}
|
|
value={skillName}
|
|
onChange={e => setSkillName(e.target.value)}
|
|
placeholder={t('skill.startTab.createModal.namePlaceholder', { ns: 'workflow' }) || ''}
|
|
destructive={isDuplicate}
|
|
disabled={isCreating}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && canCreate)
|
|
handleCreate()
|
|
}}
|
|
/>
|
|
{isDuplicate && (
|
|
<p className="text-text-destructive system-xs-regular">
|
|
{t('skill.startTab.createModal.nameDuplicate', { ns: 'workflow' })}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="mt-6 flex justify-end gap-2">
|
|
<Button
|
|
onClick={handleClose}
|
|
disabled={isCreating}
|
|
>
|
|
{t('operation.cancel', { ns: 'common' })}
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleCreate}
|
|
disabled={!canCreate}
|
|
loading={isCreating}
|
|
>
|
|
{t('operation.create', { ns: 'common' })}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export default memo(CreateBlankSkillModal)
|