mirror of
https://github.com/langgenius/dify.git
synced 2026-02-22 19:15:47 +08:00
feat: support show deleted file and folds
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
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 type { AppAssetTreeView } from '@/types/app-asset'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { RiAlertFill, RiFolderLine } from '@remixicon/react'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
@ -16,8 +17,10 @@ import {
|
||||
} 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 { START_TAB_ID } from '@/app/components/workflow/skill/constants'
|
||||
import { useSkillAssetNodeMap } from '@/app/components/workflow/skill/hooks/use-skill-asset-tree'
|
||||
import { getFileIconType } from '@/app/components/workflow/skill/utils/file-utils'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { FilePickerPanel } from '../file-picker-panel'
|
||||
import FilePreviewPanel from './file-preview-panel'
|
||||
@ -28,10 +31,16 @@ type FileReferenceBlockProps = {
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
type SkillFileMetadata = {
|
||||
files?: Record<string, AppAssetTreeView>
|
||||
}
|
||||
|
||||
const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey)
|
||||
const { data: nodeMap, isLoading: isNodeMapLoading } = useSkillAssetNodeMap()
|
||||
const activeTabId = useStore(s => s.activeTabId)
|
||||
const fileMetadata = useStore(s => s.fileMetadata)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [previewOpen, setPreviewOpen] = useState(false)
|
||||
const [previewStyle, setPreviewStyle] = useState<React.CSSProperties | null>(null)
|
||||
@ -40,7 +49,16 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) =>
|
||||
const { t } = useTranslation()
|
||||
const isInteractive = editor.isEditable()
|
||||
|
||||
const currentNode = useMemo(() => nodeMap?.get(resourceId), [nodeMap, resourceId])
|
||||
const metadataFiles = useMemo(() => {
|
||||
if (!activeTabId || activeTabId === START_TAB_ID)
|
||||
return undefined
|
||||
const metadata = fileMetadata.get(activeTabId) as SkillFileMetadata | undefined
|
||||
return metadata?.files
|
||||
}, [activeTabId, fileMetadata])
|
||||
|
||||
const treeNode = useMemo(() => nodeMap?.get(resourceId), [nodeMap, resourceId])
|
||||
const metadataNode = useMemo(() => metadataFiles?.[resourceId], [metadataFiles, resourceId])
|
||||
const currentNode = useMemo(() => treeNode ?? metadataNode, [metadataNode, treeNode])
|
||||
|
||||
const fallbackName = useMemo(() => {
|
||||
if (resourceId.includes('/')) {
|
||||
@ -50,7 +68,7 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) =>
|
||||
return resourceId.slice(0, 8)
|
||||
}, [resourceId])
|
||||
const isFolder = currentNode?.node_type === 'folder'
|
||||
const isMissing = !isNodeMapLoading && !currentNode
|
||||
const isMissing = !isNodeMapLoading && !treeNode
|
||||
const shouldPreview = isPreviewEnabled && !isFolder && !isMissing
|
||||
const displayName = currentNode?.name ?? fallbackName
|
||||
const iconType = !isFolder && currentNode
|
||||
|
||||
@ -2,6 +2,7 @@ import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { AppAssetTreeView } from '@/types/app-asset'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
@ -73,6 +74,7 @@ type ToolConfigMetadata = {
|
||||
|
||||
type SkillFileMetadata = {
|
||||
tools?: Record<string, ToolConfigMetadata>
|
||||
files?: Record<string, AppAssetTreeView>
|
||||
}
|
||||
|
||||
const getVarKindType = (type: FormTypeEnum | string) => {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { OnMount } from '@monaco-editor/react'
|
||||
import type { AppAssetTreeView } from '@/types/app-asset'
|
||||
import { loader } from '@monaco-editor/react'
|
||||
import isDeepEqual from 'fast-deep-equal'
|
||||
import dynamic from 'next/dynamic'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -26,6 +28,23 @@ import { getFileLanguage } from './utils/file-utils'
|
||||
import MediaFilePreview from './viewer/media-file-preview'
|
||||
import UnsupportedFileDownload from './viewer/unsupported-file-download'
|
||||
|
||||
type SkillFileMetadata = {
|
||||
files?: Record<string, AppAssetTreeView>
|
||||
}
|
||||
|
||||
const extractFileReferenceIds = (content: string) => {
|
||||
const ids = new Set<string>()
|
||||
const regex = /§\[file\]\.\[app\]\.\[([a-fA-F0-9-]{36})\]§/g
|
||||
let match: RegExpExecArray | null
|
||||
match = regex.exec(content)
|
||||
while (match !== null) {
|
||||
if (match[1])
|
||||
ids.add(match[1])
|
||||
match = regex.exec(content)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
const SQLiteFilePreview = dynamic(
|
||||
() => import('./viewer/sqlite-file-preview'),
|
||||
{ ssr: false, loading: () => <Loading type="area" /> },
|
||||
@ -101,6 +120,34 @@ const FileContentPanel = () => {
|
||||
clearDraftMetadata(fileTabId)
|
||||
}, [fileTabId, isMetadataDirty, fileContent, storeApi])
|
||||
|
||||
const updateFileReferenceMetadata = useCallback((content: string) => {
|
||||
if (!fileTabId)
|
||||
return
|
||||
|
||||
const referenceIds = extractFileReferenceIds(content)
|
||||
const metadata = (currentMetadata || {}) as SkillFileMetadata
|
||||
const existingFiles = metadata.files || {}
|
||||
const nextFiles: Record<string, AppAssetTreeView> = {}
|
||||
|
||||
referenceIds.forEach((id) => {
|
||||
const node = nodeMap?.get(id)
|
||||
if (node)
|
||||
nextFiles[id] = node
|
||||
else if (existingFiles[id])
|
||||
nextFiles[id] = existingFiles[id]
|
||||
})
|
||||
|
||||
const nextMetadata: SkillFileMetadata = { ...metadata }
|
||||
if (Object.keys(nextFiles).length > 0)
|
||||
nextMetadata.files = nextFiles
|
||||
else if ('files' in nextMetadata)
|
||||
delete nextMetadata.files
|
||||
|
||||
if (isDeepEqual(metadata, nextMetadata))
|
||||
return
|
||||
storeApi.getState().setDraftMetadata(fileTabId, nextMetadata)
|
||||
}, [currentMetadata, fileTabId, nodeMap, storeApi])
|
||||
|
||||
const handleEditorChange = useCallback((value: string | undefined) => {
|
||||
if (!fileTabId || !isEditable)
|
||||
return
|
||||
@ -110,9 +157,9 @@ const FileContentPanel = () => {
|
||||
storeApi.getState().clearDraftContent(fileTabId)
|
||||
else
|
||||
storeApi.getState().setDraftContent(fileTabId, newValue)
|
||||
|
||||
updateFileReferenceMetadata(newValue)
|
||||
storeApi.getState().pinTab(fileTabId)
|
||||
}, [fileTabId, isEditable, originalContent, storeApi])
|
||||
}, [fileTabId, isEditable, originalContent, storeApi, updateFileReferenceMetadata])
|
||||
|
||||
const { saveFile, registerFallback, unregisterFallback } = useSkillSaveManager()
|
||||
const handleLeaderSync = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user