mirror of
https://github.com/langgenius/dify.git
synced 2026-02-22 19:15:47 +08:00
feat: support file and fold not find
This commit is contained in:
@ -2,11 +2,12 @@ import type { LexicalNode } from 'lexical'
|
||||
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
|
||||
import type { TreeNodeData } from '@/app/components/workflow/skill/type'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { RiFolderLine } from '@remixicon/react'
|
||||
import { RiAlertFill, RiFolderLine } from '@remixicon/react'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useSkillAssetNodeMap } from '@/app/components/workflow/skill/hooks/use-skill-asset-tree'
|
||||
import { getFileIconType } from '@/app/components/workflow/skill/utils/file-utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -29,20 +31,42 @@ type FileReferenceBlockProps = {
|
||||
const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey)
|
||||
const { data: nodeMap } = useSkillAssetNodeMap()
|
||||
const { data: nodeMap, isLoading: isNodeMapLoading } = useSkillAssetNodeMap()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [previewOpen, setPreviewOpen] = useState(false)
|
||||
const [previewStyle, setPreviewStyle] = useState<React.CSSProperties | null>(null)
|
||||
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const { enabled: isPreviewEnabled } = useFilePreviewContext()
|
||||
const { t } = useTranslation()
|
||||
const isInteractive = editor.isEditable()
|
||||
|
||||
const currentNode = useMemo(() => nodeMap?.get(resourceId), [nodeMap, resourceId])
|
||||
|
||||
const fallbackName = useMemo(() => {
|
||||
if (resourceId.includes('/')) {
|
||||
const segments = resourceId.split('/').filter(Boolean)
|
||||
return segments[segments.length - 1] ?? resourceId.slice(0, 8)
|
||||
}
|
||||
return resourceId.slice(0, 8)
|
||||
}, [resourceId])
|
||||
const isFolder = currentNode?.node_type === 'folder'
|
||||
const shouldPreview = isPreviewEnabled && !isFolder
|
||||
const displayName = currentNode?.name ?? resourceId
|
||||
const iconType = !isFolder && currentNode ? getFileIconType(currentNode.name, currentNode.extension) : null
|
||||
const title = currentNode?.path ?? displayName
|
||||
const isMissing = !isNodeMapLoading && !currentNode
|
||||
const shouldPreview = isPreviewEnabled && !isFolder && !isMissing
|
||||
const displayName = currentNode?.name ?? fallbackName
|
||||
const iconType = !isFolder && currentNode
|
||||
? getFileIconType(currentNode?.name || '', currentNode?.extension)
|
||||
: null
|
||||
const pathForTooltip = (currentNode?.path ?? (resourceId.includes('/') ? resourceId : undefined))?.slice(1) // remove leading slash for better display
|
||||
const tooltipContent = isMissing
|
||||
? (
|
||||
<div className="space-y-1">
|
||||
<div className="system-xs-medium text-text-secondary">{t('skillEditor.fileNotFound', { ns: 'workflow' })}</div>
|
||||
{pathForTooltip && (
|
||||
<div>{pathForTooltip}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
: (<div className="system-xs-medium text-text-secondary">{pathForTooltip ?? displayName}</div>)
|
||||
|
||||
const handleSelect = useCallback((node: TreeNodeData) => {
|
||||
editor.update(() => {
|
||||
@ -124,35 +148,42 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) =>
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild ref={ref}>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-[18px] select-none items-center gap-[2px] overflow-hidden rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover py-[1px] pl-[1px] pr-[4px] shadow-xs',
|
||||
isInteractive ? 'cursor-pointer' : 'cursor-default',
|
||||
isSelected && 'border-text-accent',
|
||||
)}
|
||||
title={title}
|
||||
onMouseDown={() => {
|
||||
if (!isInteractive)
|
||||
return
|
||||
setOpen(prev => !prev)
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-center p-px">
|
||||
{isFolder
|
||||
? <RiFolderLine className="size-[14px] text-text-accent" aria-hidden="true" />
|
||||
: (
|
||||
<FileTypeIcon
|
||||
type={(iconType || 'document') as FileAppearanceType}
|
||||
size="sm"
|
||||
className="!size-[14px]"
|
||||
/>
|
||||
)}
|
||||
<PortalToFollowElemTrigger ref={ref} className="inline-flex">
|
||||
<Tooltip popupContent={tooltipContent} disabled={!tooltipContent}>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-[18px] select-none items-center gap-[2px] overflow-hidden rounded-[5px] border py-[1px] pl-[1px] pr-[4px] shadow-xs',
|
||||
isInteractive ? 'cursor-pointer' : 'cursor-default',
|
||||
isMissing ? 'border-state-warning-active bg-state-warning-hover' : 'border-state-accent-hover-alt bg-state-accent-hover',
|
||||
isSelected && 'border-text-accent',
|
||||
)}
|
||||
onMouseDown={() => {
|
||||
if (!isInteractive)
|
||||
return
|
||||
setOpen(prev => !prev)
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center justify-center p-px">
|
||||
{isFolder
|
||||
? <RiFolderLine className={cn('size-[14px]', isMissing ? 'text-text-warning' : 'text-text-accent')} aria-hidden="true" />
|
||||
: (
|
||||
<FileTypeIcon
|
||||
type={(iconType || 'document') as FileAppearanceType}
|
||||
size="sm"
|
||||
className={cn('!size-[14px]', isMissing && '!text-text-warning')}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className={cn('max-w-[180px] truncate text-[12px] font-medium leading-4', isMissing ? 'text-text-warning' : 'text-text-accent')}>
|
||||
{displayName}
|
||||
</span>
|
||||
{
|
||||
isMissing && (
|
||||
<RiAlertFill className="size-3 text-text-warning" />
|
||||
)
|
||||
}
|
||||
</span>
|
||||
<span className="max-w-[180px] truncate text-[12px] font-medium leading-4 text-text-accent">
|
||||
{displayName}
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1000]">
|
||||
<FilePickerPanel
|
||||
|
||||
@ -1111,6 +1111,7 @@
|
||||
"skill.startTab.useThisSkill": "Use this Skill",
|
||||
"skillEditor.authorizationBadge": "Auth",
|
||||
"skillEditor.authorizationRequired": "Authorization required before use.",
|
||||
"skillEditor.fileNotFound": "The file does not exist",
|
||||
"skillEditor.openInSkillEditor": "Open in Skill Editor",
|
||||
"skillEditor.previewUnavailable": "Preview unavailable",
|
||||
"skillEditor.referenceFiles": "Reference files",
|
||||
|
||||
@ -1103,6 +1103,7 @@
|
||||
"skill.startTab.useThisSkill": "使用此 Skill",
|
||||
"skillEditor.authorizationBadge": "Auth",
|
||||
"skillEditor.authorizationRequired": "使用前需要授权。",
|
||||
"skillEditor.fileNotFound": "文件不存在",
|
||||
"skillEditor.openInSkillEditor": "在技能编辑器中打开",
|
||||
"skillEditor.previewUnavailable": "无法预览",
|
||||
"skillEditor.referenceFiles": "引用文件",
|
||||
|
||||
Reference in New Issue
Block a user