mirror of
https://github.com/langgenius/dify.git
synced 2026-04-25 05:06:15 +08:00
feat: skill editor choose tool
This commit is contained in:
@ -41,6 +41,7 @@ type Props = {
|
||||
panelClassName?: string
|
||||
disabled: boolean
|
||||
trigger: React.ReactNode
|
||||
triggerAsChild?: boolean
|
||||
placement?: Placement
|
||||
offset?: OffsetOptions
|
||||
isShow: boolean
|
||||
@ -55,6 +56,7 @@ type Props = {
|
||||
const ToolPicker: FC<Props> = ({
|
||||
disabled,
|
||||
trigger,
|
||||
triggerAsChild = false,
|
||||
placement = 'right-start',
|
||||
offset = 0,
|
||||
isShow,
|
||||
@ -165,6 +167,7 @@ const ToolPicker: FC<Props> = ({
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={handleTriggerClick}
|
||||
asChild={triggerAsChild}
|
||||
>
|
||||
{trigger}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import SkillEditor from './skill-editor'
|
||||
|
||||
type MarkdownFileEditorProps = {
|
||||
value: string
|
||||
@ -13,7 +13,7 @@ const MarkdownFileEditor: FC<MarkdownFileEditorProps> = ({ value, onChange }) =>
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-components-panel-bg">
|
||||
<PromptEditor
|
||||
<SkillEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
showLineNumbers
|
||||
|
||||
131
web/app/components/workflow/skill/editor/skill-editor/index.tsx
Normal file
131
web/app/components/workflow/skill/editor/skill-editor/index.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
|
||||
import type { EditorState } from 'lexical'
|
||||
import type { FC } from 'react'
|
||||
import { CodeNode } from '@lexical/code'
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
|
||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
|
||||
import {
|
||||
$getRoot,
|
||||
TextNode,
|
||||
} from 'lexical'
|
||||
import * as React from 'react'
|
||||
import styles from '@/app/components/base/prompt-editor/line-numbers.module.css'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import OnBlurBlock from '@/app/components/base/prompt-editor/plugins/on-blur-or-focus-block'
|
||||
import Placeholder from '@/app/components/base/prompt-editor/plugins/placeholder'
|
||||
import UpdateBlock from '@/app/components/base/prompt-editor/plugins/update-block'
|
||||
import { textToEditorState } from '@/app/components/base/prompt-editor/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
ToolBlock,
|
||||
ToolBlockNode,
|
||||
ToolBlockReplacementBlock,
|
||||
} from './plugins/tool-block'
|
||||
import ToolPickerBlock from './plugins/tool-block/tool-picker-block'
|
||||
|
||||
export type SkillEditorProps = {
|
||||
instanceId?: string
|
||||
compact?: boolean
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
placeholder?: string | React.ReactNode
|
||||
placeholderClassName?: string
|
||||
showLineNumbers?: boolean
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
editable?: boolean
|
||||
onChange?: (text: string) => void
|
||||
onBlur?: () => void
|
||||
onFocus?: () => void
|
||||
toolPickerScope?: string
|
||||
}
|
||||
|
||||
const SkillEditor: FC<SkillEditorProps> = ({
|
||||
instanceId,
|
||||
compact,
|
||||
wrapperClassName,
|
||||
className,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
showLineNumbers,
|
||||
style,
|
||||
value,
|
||||
editable = true,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
toolPickerScope = 'all',
|
||||
}) => {
|
||||
const initialConfig = {
|
||||
namespace: 'skill-editor',
|
||||
nodes: [
|
||||
CodeNode,
|
||||
CustomTextNode,
|
||||
{
|
||||
replace: TextNode,
|
||||
with: (node: TextNode) => new CustomTextNode(node.__text),
|
||||
},
|
||||
ToolBlockNode,
|
||||
],
|
||||
editorState: textToEditorState(value || ''),
|
||||
onError: (error: Error) => {
|
||||
throw error
|
||||
},
|
||||
}
|
||||
|
||||
const handleEditorChange = (editorState: EditorState) => {
|
||||
const text = editorState.read(() => {
|
||||
return $getRoot().getChildren().map(p => p.getTextContent()).join('\n')
|
||||
})
|
||||
if (onChange)
|
||||
onChange(text)
|
||||
}
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||
<div className={cn('relative', showLineNumbers && styles.lineNumbersScope, wrapperClassName)}>
|
||||
<RichTextPlugin
|
||||
contentEditable={(
|
||||
<ContentEditable
|
||||
className={cn(
|
||||
'text-text-secondary outline-none',
|
||||
compact ? 'text-[13px] leading-5' : 'text-sm leading-6',
|
||||
showLineNumbers && styles.lineNumbers,
|
||||
className,
|
||||
)}
|
||||
style={style || {}}
|
||||
/>
|
||||
)}
|
||||
placeholder={(
|
||||
<Placeholder
|
||||
value={placeholder}
|
||||
className={cn(
|
||||
'truncate',
|
||||
showLineNumbers && styles.lineNumbersPlaceholder,
|
||||
placeholderClassName,
|
||||
)}
|
||||
compact={compact}
|
||||
/>
|
||||
)}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<>
|
||||
<ToolBlock />
|
||||
<ToolBlockReplacementBlock />
|
||||
{editable && <ToolPickerBlock scope={toolPickerScope} />}
|
||||
</>
|
||||
<OnChangePlugin onChange={handleEditorChange} />
|
||||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||
<UpdateBlock instanceId={instanceId} />
|
||||
<HistoryPlugin />
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
)
|
||||
}
|
||||
|
||||
export default SkillEditor
|
||||
@ -0,0 +1,129 @@
|
||||
import type { FC } from 'react'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useSelectOrDelete } from '@/app/components/base/prompt-editor/hooks'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { Theme } from '@/types/app'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { DELETE_TOOL_BLOCK_COMMAND } from './index'
|
||||
|
||||
type ToolBlockComponentProps = {
|
||||
nodeKey: string
|
||||
provider: string
|
||||
tool: string
|
||||
label?: string
|
||||
icon?: string | Emoji
|
||||
iconDark?: string | Emoji
|
||||
}
|
||||
|
||||
const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
|
||||
if (!icon)
|
||||
return icon
|
||||
if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`))
|
||||
return `${basePath}${icon}`
|
||||
return icon
|
||||
}
|
||||
|
||||
const ToolBlockComponent: FC<ToolBlockComponentProps> = ({
|
||||
nodeKey,
|
||||
provider,
|
||||
tool,
|
||||
label,
|
||||
icon,
|
||||
iconDark,
|
||||
}) => {
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_TOOL_BLOCK_COMMAND)
|
||||
const language = useGetLanguage()
|
||||
const { theme } = useTheme()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const toolMeta = useMemo(() => {
|
||||
const collections = [buildInTools, customTools, workflowTools, mcpTools].filter(Boolean) as ToolWithProvider[][]
|
||||
for (const collection of collections) {
|
||||
const providerItem = collection.find(item => item.name === provider || item.id === provider || canFindTool(item.id, provider))
|
||||
if (!providerItem)
|
||||
continue
|
||||
const toolItem = providerItem.tools?.find(item => item.name === tool)
|
||||
if (!toolItem)
|
||||
continue
|
||||
return {
|
||||
label: toolItem.label?.[language] || tool,
|
||||
icon: providerItem.icon,
|
||||
iconDark: providerItem.icon_dark,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [buildInTools, customTools, workflowTools, mcpTools, language, provider, tool])
|
||||
|
||||
const displayLabel = label || toolMeta?.label || tool
|
||||
const resolvedIcon = (() => {
|
||||
const fromNode = theme === Theme.dark ? iconDark : icon
|
||||
if (fromNode)
|
||||
return normalizeProviderIcon(fromNode)
|
||||
const fromMeta = theme === Theme.dark ? toolMeta?.iconDark : toolMeta?.icon
|
||||
return normalizeProviderIcon(fromMeta)
|
||||
})()
|
||||
|
||||
const renderIcon = () => {
|
||||
if (!resolvedIcon)
|
||||
return null
|
||||
if (typeof resolvedIcon === 'string') {
|
||||
if (resolvedIcon.startsWith('http') || resolvedIcon.startsWith('/')) {
|
||||
return (
|
||||
<span
|
||||
className="h-[14px] w-[14px] shrink-0 rounded-[3px] bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${resolvedIcon})` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<AppIcon
|
||||
size="xs"
|
||||
icon={resolvedIcon}
|
||||
className="!h-[14px] !w-[14px] shrink-0 !border-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<AppIcon
|
||||
size="xs"
|
||||
icon={resolvedIcon.content}
|
||||
background={resolvedIcon.background}
|
||||
className="!h-[14px] !w-[14px] shrink-0 !border-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-[2px] rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover px-[4px] py-[1px] shadow-xs',
|
||||
isSelected && 'border-text-accent',
|
||||
)}
|
||||
title={`${provider}.${tool}`}
|
||||
>
|
||||
{renderIcon()}
|
||||
<span className="system-xs-medium max-w-[180px] truncate text-text-accent">
|
||||
{displayLabel}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ToolBlockComponent)
|
||||
@ -0,0 +1,46 @@
|
||||
import type { ToolBlockPayload } from './node'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { memo, useEffect } from 'react'
|
||||
import { $createToolBlockNode, ToolBlockNode } from './node'
|
||||
|
||||
export const INSERT_TOOL_BLOCK_COMMAND = createCommand<ToolBlockPayload>('INSERT_TOOL_BLOCK_COMMAND')
|
||||
export const DELETE_TOOL_BLOCK_COMMAND = createCommand('DELETE_TOOL_BLOCK_COMMAND')
|
||||
|
||||
const ToolBlock = memo(() => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ToolBlockNode]))
|
||||
throw new Error('ToolBlockPlugin: ToolBlockNode not registered on editor')
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
INSERT_TOOL_BLOCK_COMMAND,
|
||||
(payload: ToolBlockPayload) => {
|
||||
const toolBlockNode = $createToolBlockNode(payload)
|
||||
$insertNodes([toolBlockNode])
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DELETE_TOOL_BLOCK_COMMAND,
|
||||
() => true,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
})
|
||||
ToolBlock.displayName = 'ToolBlock'
|
||||
|
||||
export { ToolBlock }
|
||||
export { ToolBlockNode } from './node'
|
||||
export { default as ToolBlockReplacementBlock } from './tool-block-replacement-block'
|
||||
@ -0,0 +1,115 @@
|
||||
import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import { DecoratorNode } from 'lexical'
|
||||
import ToolBlockComponent from './component'
|
||||
import { buildToolToken } from './utils'
|
||||
|
||||
export type ToolBlockPayload = {
|
||||
provider: string
|
||||
tool: string
|
||||
configId: string
|
||||
label?: string
|
||||
icon?: string | Emoji
|
||||
iconDark?: string | Emoji
|
||||
}
|
||||
|
||||
export type SerializedToolBlockNode = SerializedLexicalNode & ToolBlockPayload
|
||||
|
||||
export class ToolBlockNode extends DecoratorNode<React.JSX.Element> {
|
||||
__provider: string
|
||||
__tool: string
|
||||
__configId: string
|
||||
__label?: string
|
||||
__icon?: string | Emoji
|
||||
__iconDark?: string | Emoji
|
||||
|
||||
static getType(): string {
|
||||
return 'tool-block'
|
||||
}
|
||||
|
||||
static clone(node: ToolBlockNode): ToolBlockNode {
|
||||
return new ToolBlockNode(
|
||||
{
|
||||
provider: node.__provider,
|
||||
tool: node.__tool,
|
||||
configId: node.__configId,
|
||||
label: node.__label,
|
||||
icon: node.__icon,
|
||||
iconDark: node.__iconDark,
|
||||
},
|
||||
node.__key,
|
||||
)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
constructor(payload: ToolBlockPayload, key?: NodeKey) {
|
||||
super(key)
|
||||
this.__provider = payload.provider
|
||||
this.__tool = payload.tool
|
||||
this.__configId = payload.configId
|
||||
this.__label = payload.label
|
||||
this.__icon = payload.icon
|
||||
this.__iconDark = payload.iconDark
|
||||
}
|
||||
|
||||
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 (
|
||||
<ToolBlockComponent
|
||||
nodeKey={this.getKey()}
|
||||
provider={this.__provider}
|
||||
tool={this.__tool}
|
||||
label={this.__label}
|
||||
icon={this.__icon}
|
||||
iconDark={this.__iconDark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
exportJSON(): SerializedToolBlockNode {
|
||||
return {
|
||||
type: 'tool-block',
|
||||
version: 1,
|
||||
provider: this.__provider,
|
||||
tool: this.__tool,
|
||||
configId: this.__configId,
|
||||
label: this.__label,
|
||||
icon: this.__icon,
|
||||
iconDark: this.__iconDark,
|
||||
}
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedToolBlockNode): ToolBlockNode {
|
||||
return $createToolBlockNode(serializedNode)
|
||||
}
|
||||
|
||||
getTextContent(): string {
|
||||
return buildToolToken({
|
||||
provider: this.__provider,
|
||||
tool: this.__tool,
|
||||
configId: this.__configId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function $createToolBlockNode(payload: ToolBlockPayload): ToolBlockNode {
|
||||
return new ToolBlockNode(payload)
|
||||
}
|
||||
|
||||
export function $isToolBlockNode(
|
||||
node: ToolBlockNode | LexicalNode | null | undefined,
|
||||
): node is ToolBlockNode {
|
||||
return node instanceof ToolBlockNode
|
||||
}
|
||||
@ -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 { $createToolBlockNode, ToolBlockNode } from './node'
|
||||
import { getToolTokenRegexString, parseToolToken } from './utils'
|
||||
|
||||
const ToolBlockReplacementBlock = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const regex = useMemo(() => new RegExp(getToolTokenRegexString(), 'i'), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ToolBlockNode]))
|
||||
throw new Error('ToolBlockReplacementBlock: ToolBlockNode 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 createToolBlockNode = (textNode: CustomTextNode) => {
|
||||
const parsed = parseToolToken(textNode.getTextContent())
|
||||
if (!parsed)
|
||||
return $createTextNode(textNode.getTextContent())
|
||||
return $createToolBlockNode(parsed)
|
||||
}
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createToolBlockNode)),
|
||||
)
|
||||
}, [editor, regex])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default ToolBlockReplacementBlock
|
||||
@ -0,0 +1,115 @@
|
||||
import type { LexicalNode } from 'lexical'
|
||||
import type { FC } from 'react'
|
||||
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { LexicalTypeaheadMenuPlugin, MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||
import {
|
||||
$createTextNode,
|
||||
$insertNodes,
|
||||
} from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { useBasicTypeaheadTriggerMatch } from '@/app/components/base/prompt-editor/hooks'
|
||||
import { $splitNodeContainingQuery } from '@/app/components/base/prompt-editor/utils'
|
||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||
import { $createToolBlockNode } from './node'
|
||||
|
||||
class ToolPickerMenuOption extends MenuOption {
|
||||
constructor() {
|
||||
super('tool-picker')
|
||||
}
|
||||
}
|
||||
|
||||
type ToolPickerBlockProps = {
|
||||
scope?: string
|
||||
}
|
||||
|
||||
const ToolPickerBlock: FC<ToolPickerBlockProps> = ({ scope = 'all' }) => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', {
|
||||
minLength: 0,
|
||||
maxLength: 0,
|
||||
})
|
||||
|
||||
const options = useMemo(() => [new ToolPickerMenuOption()], [])
|
||||
|
||||
const insertTools = useCallback((tools: ToolDefaultValue[]) => {
|
||||
editor.update(() => {
|
||||
const match = checkForTriggerMatch('@', editor)
|
||||
const nodeToRemove = match ? $splitNodeContainingQuery(match) : null
|
||||
if (nodeToRemove)
|
||||
nodeToRemove.remove()
|
||||
|
||||
const nodes: LexicalNode[] = []
|
||||
tools.forEach((tool, index) => {
|
||||
nodes.push(
|
||||
$createToolBlockNode({
|
||||
provider: tool.provider_name,
|
||||
tool: tool.tool_name,
|
||||
configId: uuid(),
|
||||
label: tool.tool_label,
|
||||
icon: tool.provider_icon,
|
||||
iconDark: tool.provider_icon_dark,
|
||||
}),
|
||||
)
|
||||
if (index !== tools.length - 1)
|
||||
nodes.push($createTextNode(' '))
|
||||
})
|
||||
|
||||
if (nodes.length)
|
||||
$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(
|
||||
<ToolPicker
|
||||
disabled={false}
|
||||
trigger={(
|
||||
<span className="inline-block h-0 w-0" />
|
||||
)}
|
||||
triggerAsChild
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
isShow
|
||||
onShowChange={(isShow) => {
|
||||
if (!isShow)
|
||||
closeMenu()
|
||||
}}
|
||||
onSelect={(tool) => {
|
||||
insertTools([tool])
|
||||
closeMenu()
|
||||
}}
|
||||
onSelectMultiple={(tools) => {
|
||||
insertTools(tools)
|
||||
closeMenu()
|
||||
}}
|
||||
scope={scope}
|
||||
/>,
|
||||
anchorElementRef.current,
|
||||
)
|
||||
}, [insertTools, options, scope])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={options}
|
||||
onSelectOption={() => {}}
|
||||
onQueryChange={() => {}}
|
||||
menuRenderFn={renderMenu}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
anchorClassName="z-[999999] translate-y-[calc(-100%-3px)]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ToolPickerBlock)
|
||||
@ -0,0 +1,18 @@
|
||||
export const getToolTokenRegexString = (): string => {
|
||||
return '§tool\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-fA-F0-9-]{36}§'
|
||||
}
|
||||
|
||||
export const parseToolToken = (text: string) => {
|
||||
const match = /^§tool\.([\w-]+)\.([\w-]+)\.([a-fA-F0-9-]{36})§$/.exec(text)
|
||||
if (!match)
|
||||
return null
|
||||
return {
|
||||
provider: match[1],
|
||||
tool: match[2],
|
||||
configId: match[3],
|
||||
}
|
||||
}
|
||||
|
||||
export const buildToolToken = (payload: { provider: string, tool: string, configId: string }) => {
|
||||
return `§tool.${payload.provider}.${payload.tool}.${payload.configId}§`
|
||||
}
|
||||
Reference in New Issue
Block a user