feat: support show deleted file and folds

This commit is contained in:
Joel
2026-02-06 14:20:25 +08:00
parent a0984a779f
commit 3eba0c561e
3 changed files with 71 additions and 4 deletions

View File

@ -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

View File

@ -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) => {

View File

@ -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(() => {