mirror of
https://github.com/langgenius/dify.git
synced 2026-05-11 06:37:13 +08:00
feat: support choose folders and files
This commit is contained in:
parent
0d5e971a0c
commit
3a775fc2bf
@ -21,6 +21,9 @@ import UpdateBlock from '@/app/components/base/prompt-editor/plugins/update-bloc
|
||||
import { textToEditorState } from '@/app/components/base/prompt-editor/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import styles from './line-numbers.module.css'
|
||||
import FilePickerBlock from './plugins/file-picker-block'
|
||||
import { FileReferenceNode } from './plugins/file-reference-block/node'
|
||||
import FileReferenceReplacementBlock from './plugins/file-reference-block/replacement-block'
|
||||
import {
|
||||
ToolBlock,
|
||||
ToolBlockNode,
|
||||
@ -71,6 +74,7 @@ const SkillEditor: FC<SkillEditorProps> = ({
|
||||
with: (node: TextNode) => new CustomTextNode(node.__text),
|
||||
},
|
||||
ToolBlockNode,
|
||||
FileReferenceNode,
|
||||
],
|
||||
editorState: textToEditorState(value || ''),
|
||||
onError: (error: Error) => {
|
||||
@ -120,6 +124,8 @@ const SkillEditor: FC<SkillEditorProps> = ({
|
||||
<>
|
||||
<ToolBlock />
|
||||
<ToolBlockReplacementBlock />
|
||||
<FileReferenceReplacementBlock />
|
||||
{editable && <FilePickerBlock />}
|
||||
{editable && <ToolPickerBlock scope={toolPickerScope} />}
|
||||
</>
|
||||
<OnChangePlugin onChange={handleEditorChange} />
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import type { LexicalNode } from 'lexical'
|
||||
import type { FC } from 'react'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { LexicalTypeaheadMenuPlugin, MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import {
|
||||
$insertNodes,
|
||||
} from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useBasicTypeaheadTriggerMatch } from '@/app/components/base/prompt-editor/hooks'
|
||||
import { $splitNodeContainingQuery } from '@/app/components/base/prompt-editor/utils'
|
||||
import { FilePickerPanel } from './file-picker-panel'
|
||||
import { $createFileReferenceNode } from './file-reference-block/node'
|
||||
|
||||
class FilePickerMenuOption extends MenuOption {
|
||||
constructor() {
|
||||
super('file-picker')
|
||||
}
|
||||
}
|
||||
|
||||
const FilePickerBlock: FC = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
minLength: 0,
|
||||
maxLength: 0,
|
||||
})
|
||||
|
||||
const options = useMemo(() => [new FilePickerMenuOption()], [])
|
||||
|
||||
const insertFileReference = useCallback((resourceId: string) => {
|
||||
editor.update(() => {
|
||||
const match = checkForTriggerMatch('/', editor)
|
||||
const nodeToRemove = match ? $splitNodeContainingQuery(match) : null
|
||||
if (nodeToRemove)
|
||||
nodeToRemove.remove()
|
||||
|
||||
const nodes: LexicalNode[] = [$createFileReferenceNode({ resourceId })]
|
||||
$insertNodes(nodes)
|
||||
})
|
||||
}, [checkForTriggerMatch, editor])
|
||||
|
||||
const renderMenu = useCallback((
|
||||
anchorElementRef: React.RefObject<HTMLElement | null>,
|
||||
{ selectOptionAndCleanUp }: { selectOptionAndCleanUp: (option: MenuOption) => void },
|
||||
) => {
|
||||
if (!anchorElementRef.current)
|
||||
return null
|
||||
|
||||
const closeMenu = () => selectOptionAndCleanUp(options[0])
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<PortalToFollowElem
|
||||
open
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild>
|
||||
<span className="inline-block h-0 w-0" />
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1000]">
|
||||
<FilePickerPanel
|
||||
onSelectNode={(node) => {
|
||||
insertFileReference(node.id)
|
||||
closeMenu()
|
||||
}}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
}, [insertFileReference, options])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={options}
|
||||
onSelectOption={() => { }}
|
||||
onQueryChange={() => { }}
|
||||
menuRenderFn={renderMenu}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
anchorClassName="z-[999999] translate-y-[calc(-100%-3px)]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FilePickerBlock)
|
||||
export { FilePickerPanel }
|
||||
@ -0,0 +1,187 @@
|
||||
import type { FC } from 'react'
|
||||
import type { NodeRendererProps } from 'react-arborist'
|
||||
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
|
||||
import type { TreeNodeData } from '@/app/components/workflow/skill/type'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiFolderLine, RiFolderOpenLine, RiQuestionLine } from '@remixicon/react'
|
||||
import { useSize } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { Tree } from 'react-arborist'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import TreeGuideLines from '@/app/components/workflow/skill/file-tree/tree-guide-lines'
|
||||
import { useSkillAssetTreeData } from '@/app/components/workflow/skill/hooks/use-skill-asset-tree'
|
||||
import { getFileIconType } from '@/app/components/workflow/skill/utils/file-utils'
|
||||
import { toOpensObject } from '@/app/components/workflow/skill/utils/tree-utils'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type FilePickerTreeNodeProps = NodeRendererProps<TreeNodeData> & {
|
||||
onSelectNode: (node: TreeNodeData) => void
|
||||
}
|
||||
|
||||
const FilePickerTreeNode: FC<FilePickerTreeNodeProps> = ({ node, style, dragHandle, onSelectNode }) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
const isFolder = node.data.node_type === 'folder'
|
||||
const isSelected = node.isSelected
|
||||
const fileIconType = !isFolder ? getFileIconType(node.data.name) : null
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
node.select()
|
||||
onSelectNode(node.data)
|
||||
}, [node, onSelectNode])
|
||||
|
||||
const handleToggle = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
node.toggle()
|
||||
}, [node])
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onSelectNode(node.data)
|
||||
}
|
||||
}, [node, onSelectNode])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dragHandle}
|
||||
style={style}
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
aria-selected={isSelected}
|
||||
aria-expanded={isFolder ? node.isOpen : undefined}
|
||||
className={cn(
|
||||
'group relative flex h-6 cursor-pointer items-center gap-px overflow-hidden rounded-md',
|
||||
'hover:bg-state-base-hover',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
|
||||
isSelected && 'bg-state-base-active',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<TreeGuideLines level={node.level} />
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 px-3">
|
||||
<div className="flex size-4 shrink-0 items-center justify-center">
|
||||
{isFolder
|
||||
? (
|
||||
node.isOpen
|
||||
? <RiFolderOpenLine className="size-4 text-text-accent" aria-hidden="true" />
|
||||
: <RiFolderLine className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
)
|
||||
: (
|
||||
<FileTypeIcon type={fileIconType as FileAppearanceType} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate text-[13px] font-normal leading-4',
|
||||
isSelected ? 'text-text-primary' : 'text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{node.data.name}
|
||||
</span>
|
||||
</div>
|
||||
{isFolder && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={handleToggle}
|
||||
aria-label={t('skillSidebar.toggleFolder')}
|
||||
className={cn(
|
||||
'flex size-6 shrink-0 items-center justify-center rounded-md',
|
||||
'text-text-tertiary hover:bg-state-base-hover-alt',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
|
||||
)}
|
||||
>
|
||||
{node.isOpen
|
||||
? <RiArrowDownSLine className="size-4" aria-hidden="true" />
|
||||
: <RiArrowRightSLine className="size-4" aria-hidden="true" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
FilePickerTreeNode.displayName = 'FilePickerTreeNode'
|
||||
|
||||
type FilePickerPanelProps = {
|
||||
onSelectNode: (node: TreeNodeData) => void
|
||||
}
|
||||
|
||||
const FilePickerPanel: FC<FilePickerPanelProps> = ({ onSelectNode }) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
|
||||
const expandedFolderIds = useStore(s => s.expandedFolderIds)
|
||||
const storeApi = useWorkflowStore()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const containerSize = useSize(containerRef)
|
||||
|
||||
const treeNodes = useMemo(() => treeData?.children || [], [treeData?.children])
|
||||
const initialOpenState = useMemo(() => toOpensObject(expandedFolderIds), [expandedFolderIds])
|
||||
|
||||
const renderNode = useCallback((props: NodeRendererProps<TreeNodeData>) => (
|
||||
<FilePickerTreeNode {...props} onSelectNode={onSelectNode} />
|
||||
), [onSelectNode])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[280px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm"
|
||||
onMouseDown={(e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('input, textarea, select'))
|
||||
return
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1 px-4 pb-1 pt-1.5">
|
||||
<span className="flex-1 text-[12px] font-medium uppercase leading-4 text-text-tertiary">
|
||||
{t('skillEditor.referenceFiles')}
|
||||
</span>
|
||||
<RiQuestionLine className="size-4 text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
<div ref={containerRef} className="max-h-[320px] min-h-[120px] px-2 pb-2">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center py-6">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && error && (
|
||||
<div className="flex items-center justify-center py-6 text-[12px] text-text-tertiary">
|
||||
{t('skillSidebar.loadError')}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && treeNodes.length === 0 && (
|
||||
<div className="flex items-center justify-center py-6 text-[12px] text-text-tertiary">
|
||||
{t('skillSidebar.empty')}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && treeNodes.length > 0 && (
|
||||
<Tree<TreeNodeData>
|
||||
data={treeNodes}
|
||||
idAccessor="id"
|
||||
childrenAccessor="children"
|
||||
width="100%"
|
||||
height={containerSize?.height ?? 240}
|
||||
rowHeight={24}
|
||||
indent={20}
|
||||
overscanCount={5}
|
||||
openByDefault={false}
|
||||
initialOpenState={initialOpenState}
|
||||
onToggle={(id) => {
|
||||
storeApi.getState().toggleFolder(id)
|
||||
}}
|
||||
disableDrag
|
||||
disableDrop
|
||||
>
|
||||
{renderNode}
|
||||
</Tree>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { FilePickerPanel }
|
||||
@ -0,0 +1,90 @@
|
||||
import type { LexicalNode } from 'lexical'
|
||||
import type { FC } from 'react'
|
||||
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 { $getNodeByKey } from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks'
|
||||
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'
|
||||
import { FilePickerPanel } from '../file-picker-panel'
|
||||
|
||||
type FileReferenceBlockProps = {
|
||||
nodeKey: string
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
const FileReferenceBlock: FC<FileReferenceBlockProps> = ({ nodeKey, resourceId }) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey)
|
||||
const { data: nodeMap } = useSkillAssetNodeMap()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const currentNode = useMemo(() => nodeMap?.get(resourceId), [nodeMap, resourceId])
|
||||
const isFolder = currentNode?.node_type === 'folder'
|
||||
const displayName = currentNode?.name ?? resourceId
|
||||
const iconType = !isFolder && currentNode ? getFileIconType(currentNode.name) : null
|
||||
const title = currentNode?.path ?? displayName
|
||||
|
||||
const handleSelect = useCallback((node: TreeNodeData) => {
|
||||
editor.update(() => {
|
||||
const targetNode = $getNodeByKey(nodeKey)
|
||||
if (targetNode?.getType() === 'file-reference-block') {
|
||||
const fileNode = targetNode as LexicalNode & { setResourceId?: (resourceId: string) => void }
|
||||
fileNode.setResourceId?.(node.id)
|
||||
}
|
||||
})
|
||||
setOpen(false)
|
||||
}, [editor, nodeKey])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-[18px] cursor-pointer 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',
|
||||
isSelected && 'border-text-accent',
|
||||
)}
|
||||
ref={ref}
|
||||
title={title}
|
||||
onMouseDown={() => 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]"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="max-w-[180px] truncate text-[12px] font-medium leading-4 text-text-accent">
|
||||
{displayName}
|
||||
</span>
|
||||
</span>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1000]">
|
||||
<FilePickerPanel onSelectNode={handleSelect} />
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FileReferenceBlock)
|
||||
@ -0,0 +1,81 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import FileReferenceBlock from './component'
|
||||
import { buildFileReferenceToken } from './utils'
|
||||
|
||||
export type FileReferencePayload = {
|
||||
resourceId: string
|
||||
}
|
||||
|
||||
export type SerializedFileReferenceNode = SerializedLexicalNode & FileReferencePayload
|
||||
|
||||
export class FileReferenceNode extends DecoratorNode<React.JSX.Element> {
|
||||
__resourceId: string
|
||||
|
||||
static getType(): string {
|
||||
return 'file-reference-block'
|
||||
}
|
||||
|
||||
static clone(node: FileReferenceNode): FileReferenceNode {
|
||||
return new FileReferenceNode({ resourceId: node.__resourceId }, node.__key)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(payload: FileReferencePayload, key?: NodeKey) {
|
||||
super(key)
|
||||
this.__resourceId = payload.resourceId
|
||||
}
|
||||
|
||||
createDOM(): HTMLElement {
|
||||
const span = document.createElement('span')
|
||||
span.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return span
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
decorate(): React.JSX.Element {
|
||||
return (
|
||||
<FileReferenceBlock
|
||||
nodeKey={this.getKey()}
|
||||
resourceId={this.__resourceId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
setResourceId(resourceId: string): void {
|
||||
const writable = this.getWritable()
|
||||
writable.__resourceId = resourceId
|
||||
}
|
||||
|
||||
exportJSON(): SerializedFileReferenceNode {
|
||||
return {
|
||||
type: 'file-reference-block',
|
||||
version: 1,
|
||||
resourceId: this.__resourceId,
|
||||
}
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedFileReferenceNode): FileReferenceNode {
|
||||
return $createFileReferenceNode(serializedNode)
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return buildFileReferenceToken(this.__resourceId)
|
||||
}
|
||||
}
|
||||
|
||||
export function $createFileReferenceNode(payload: FileReferencePayload): FileReferenceNode {
|
||||
return new FileReferenceNode(payload)
|
||||
}
|
||||
|
||||
export function $isFileReferenceNode(
|
||||
node: FileReferenceNode | LexicalNode | null | undefined,
|
||||
): node is FileReferenceNode {
|
||||
return node instanceof FileReferenceNode
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { $createTextNode } from 'lexical'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import { decoratorTransform } from '@/app/components/base/prompt-editor/utils'
|
||||
import { $createFileReferenceNode, FileReferenceNode } from './node'
|
||||
import { getFileReferenceTokenRegexString, parseFileReferenceToken } from './utils'
|
||||
|
||||
const FileReferenceReplacementBlock = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const regex = useMemo(() => new RegExp(getFileReferenceTokenRegexString(), 'i'), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([FileReferenceNode]))
|
||||
throw new Error('FileReferenceReplacementBlock: FileReferenceNode not registered on editor')
|
||||
|
||||
const getMatch = (text: string) => {
|
||||
const matchArr = regex.exec(text)
|
||||
if (!matchArr)
|
||||
return null
|
||||
return {
|
||||
start: matchArr.index,
|
||||
end: matchArr.index + matchArr[0].length,
|
||||
}
|
||||
}
|
||||
|
||||
const createFileReferenceNode = (textNode: CustomTextNode) => {
|
||||
const parsed = parseFileReferenceToken(textNode.getTextContent())
|
||||
if (!parsed)
|
||||
return $createTextNode(textNode.getTextContent())
|
||||
return $createFileReferenceNode(parsed)
|
||||
}
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createFileReferenceNode)),
|
||||
)
|
||||
}, [editor, regex])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default FileReferenceReplacementBlock
|
||||
@ -0,0 +1,16 @@
|
||||
export const getFileReferenceTokenRegexString = (): string => {
|
||||
return '§\\[file\\]\\.\\[app\\]\\.\\[[a-fA-F0-9-]{36}\\]§'
|
||||
}
|
||||
|
||||
export const parseFileReferenceToken = (text: string) => {
|
||||
const match = /^§\[file\]\.\[app\]\.\[([a-fA-F0-9-]{36})\]§$/.exec(text)
|
||||
if (!match)
|
||||
return null
|
||||
return {
|
||||
resourceId: match[1],
|
||||
}
|
||||
}
|
||||
|
||||
export const buildFileReferenceToken = (resourceId: string) => {
|
||||
return `§[file].[app].[${resourceId}]§`
|
||||
}
|
||||
@ -997,6 +997,7 @@
|
||||
"singleRun.testRunLoop": "Test Run Loop",
|
||||
"skillEditor.officePlaceholder": "Preview will be supported in a future update",
|
||||
"skillEditor.previewUnavailable": "Preview unavailable",
|
||||
"skillEditor.referenceFiles": "Reference files",
|
||||
"skillEditor.unsupportedPreview": "This file type is not supported for preview",
|
||||
"skillSidebar.addFile": "Upload File",
|
||||
"skillSidebar.addFolder": "New Folder",
|
||||
|
||||
@ -991,6 +991,7 @@
|
||||
"singleRun.testRunLoop": "测试运行循环",
|
||||
"skillEditor.officePlaceholder": "预览功能将在后续版本支持",
|
||||
"skillEditor.previewUnavailable": "无法预览",
|
||||
"skillEditor.referenceFiles": "引用文件",
|
||||
"skillEditor.unsupportedPreview": "该文件类型不支持预览",
|
||||
"skillSidebar.addFile": "上传文件",
|
||||
"skillSidebar.addFolder": "新建文件夹",
|
||||
|
||||
@ -980,6 +980,7 @@
|
||||
"singleRun.testRun": "測試運行",
|
||||
"singleRun.testRunIteration": "測試運行迭代",
|
||||
"singleRun.testRunLoop": "測試運行循環",
|
||||
"skillEditor.referenceFiles": "參考檔案",
|
||||
"tabs.-": "預設",
|
||||
"tabs.addAll": "全部新增",
|
||||
"tabs.agent": "代理策略",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user