= ({showBanner=true,pageTitle,tagList,showB
{showBtn && }
+ {customBtn}
{description}
diff --git a/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx b/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx
index 66ef75ae..45455e89 100644
--- a/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx
+++ b/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx
@@ -2,7 +2,7 @@ import {App, Col, Form, Input, Row, Table, Tooltip} from "antd";
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react";
import {PublishApprovalInfoType, PublishApprovalModalHandle, PublishApprovalModalProps, PublishVersionTableListItem} from "@common/const/approval/type.tsx";
import {useFetch} from "@common/hooks/http.ts";
-import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR, VALIDATE_MESSAGE} from "@common/const/const.tsx";
+import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR} from "@common/const/const.tsx";
import WithPermission from "@common/components/aoplatform/WithPermission.tsx";
import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "@core/const/system/const.tsx";
import { $t } from "@common/locales";
@@ -14,7 +14,7 @@ import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/Syste
export const PublishApprovalModalContent = forwardRef((props, ref) => {
const { message } = App.useApp()
- const { type,data,insideSystem = false,serviceId, teamId} = props
+ const { type,data,insidePage = false, serviceType = 'rest', serviceId, teamId} = props
const [form] = Form.useForm();
const {fetchData} = useFetch()
const {state} = useGlobalContext()
@@ -109,7 +109,7 @@ export const PublishApprovalModalContent = forwardRefApprovalRouteColumns.map((x)=>({
+ const translatedRouteColumns = useMemo(()=>ApprovalRouteColumns.filter(x=> serviceType === 'rest' ? x.dataIndex !== 'name' : x.dataIndex !== 'methods').map((x)=>({
...x,
...(x.dataIndex === 'change' ? {
render:(_,entity)=>(
@@ -122,7 +122,7 @@ export const PublishApprovalModalContent = forwardRefSYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{
if(x.dataIndex === 'status'){
@@ -142,7 +142,7 @@ export const PublishApprovalModalContent = forwardRef
- {!insideSystem && <>
+ {!insidePage && <>
{$t('申请系统')}:
{(data as PublishApprovalInfoType).project || '-'}
@@ -177,7 +177,7 @@ export const PublishApprovalModalContent = forwardRef
{
- insideSystem &&
+ insidePage &&
<>
- {$t('上游列表')}:
-
-
+ {
+ serviceType === 'rest' && <>
+ {$t('上游列表')}:
+
+
+ >
+ }
{
{$t('替换文件')}
{$t('是否放行')}
{$t('监控')}
+ {$t('必填')}
+ {$t('字符非法,仅支持英文')}
>
)
}
\ No newline at end of file
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditor.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditor.tsx
new file mode 100644
index 00000000..20320bb3
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditor.tsx
@@ -0,0 +1,221 @@
+'use client'
+
+import type { FC } from 'react'
+import { useEffect, useMemo } from 'react'
+import type {
+ EditorState,
+} from 'lexical'
+import {
+ $getRoot,
+ TextNode,
+} from 'lexical'
+import { CodeNode } from '@lexical/code'
+import { LexicalComposer } from '@lexical/react/LexicalComposer'
+import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
+import { ContentEditable } from '@lexical/react/LexicalContentEditable'
+import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
+import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
+import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
+// import TreeView from './plugins/tree-view'
+import Placeholder from './plugins/placeholder'
+import ComponentPickerBlock from './plugins/component-picker-block/index'
+import {
+ ContextBlock,
+ ContextBlockNode,
+ ContextBlockReplacementBlock,
+} from './plugins/context-block/index'
+import {
+ QueryBlock,
+ QueryBlockNode,
+ QueryBlockReplacementBlock,
+} from './plugins/query-block/index'
+import {
+ HistoryBlock,
+ HistoryBlockNode,
+ HistoryBlockReplacementBlock,
+} from './plugins/history-block/index'
+import {
+ WorkflowVariableBlock,
+ WorkflowVariableBlockNode,
+ WorkflowVariableBlockReplacementBlock,
+} from './plugins/workflow-variable-block/index'
+import VariableBlock from './plugins/variable-block/index'
+import VariableValueBlock from './plugins/variable-value-block/index'
+import { VariableValueBlockNode } from './plugins/variable-value-block/node'
+import { CustomTextNode } from './plugins/custom-text/node'
+import OnBlurBlock from './plugins/on-blur-or-focus-block'
+import UpdateBlock from './plugins/update-block'
+import { textToEditorState } from './utils'
+import type {
+ ContextBlockType,
+ ExternalToolBlockType,
+ HistoryBlockType,
+ QueryBlockType,
+ VariableBlockType,
+ WorkflowVariableBlockType,
+} from './types'
+import {
+ UPDATE_DATASETS_EVENT_EMITTER,
+ UPDATE_HISTORY_EVENT_EMITTER,
+} from './constants'
+import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
+
+export type PromptEditorProps = {
+ instanceId?: string
+ compact?: boolean
+ className?: string
+ placeholder?: string
+ placeholderClassName?: string
+ style?: React.CSSProperties
+ value?: string
+ editable?: boolean
+ onChange?: (text: string) => void
+ onBlur?: () => void
+ onFocus?: () => void
+ contextBlock?: ContextBlockType
+ queryBlock?: QueryBlockType
+ historyBlock?: HistoryBlockType
+ variableBlock?: VariableBlockType
+ externalToolBlock?: ExternalToolBlockType
+ workflowVariableBlock?: WorkflowVariableBlockType
+}
+
+const PromptEditor: FC = ({
+ instanceId,
+ compact,
+ className,
+ placeholder,
+ placeholderClassName,
+ style,
+ value,
+ editable = true,
+ onChange,
+ onBlur,
+ onFocus,
+ contextBlock,
+ queryBlock,
+ historyBlock,
+ variableBlock,
+ externalToolBlock,
+ workflowVariableBlock,
+}) => {
+ const { eventEmitter } = useEventEmitterContextContext()
+ const initialConfig = {
+ namespace: 'prompt-editor',
+ nodes: [
+ CodeNode,
+ CustomTextNode,
+ {
+ replace: TextNode,
+ with: (node: TextNode) => new CustomTextNode(node.__text),
+ },
+ ContextBlockNode,
+ HistoryBlockNode,
+ QueryBlockNode,
+ WorkflowVariableBlockNode,
+ VariableValueBlockNode,
+ ],
+ 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)
+ }
+
+ useEffect(() => {
+ eventEmitter?.emit({
+ type: UPDATE_DATASETS_EVENT_EMITTER,
+ payload: contextBlock?.datasets,
+ } as any)
+ }, [eventEmitter, contextBlock?.datasets])
+ useEffect(() => {
+ eventEmitter?.emit({
+ type: UPDATE_HISTORY_EVENT_EMITTER,
+ payload: historyBlock?.history,
+ } as any)
+ }, [eventEmitter, historyBlock?.history])
+
+ return (
+
+
+
}
+ placeholder={
}
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+
+
+ {
+ contextBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ queryBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ historyBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ (variableBlock?.show || externalToolBlock?.show) && (
+ <>
+
+
+ >
+ )
+ }
+ {
+ workflowVariableBlock?.show && (
+ <>
+
+
+ >
+ )
+ }
+
+
+
+
+ {/*
*/}
+
+
+ )
+}
+
+export default PromptEditor
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx
new file mode 100644
index 00000000..24e37ebf
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx
@@ -0,0 +1,82 @@
+
+import PromptEditor from '@common/components/aoplatform/prompt-editor/PromptEditor.tsx';
+import PromptEditorHeightResizeWrap from '@common/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx';
+import { useEffect, useState } from 'react';
+import { getVars } from './utils';
+import { VariableItems } from '@core/const/ai-service/type';
+
+const PromptEditorResizable = (props:{value?:string, onChange?:(value:string)=>void, variablesChange?:(keys:string[])=>void,promptVariables:VariableItems[]}) =>{
+ const {value , onChange,variablesChange,promptVariables} = props
+ const minHeight = 68
+ const [editorHeight, setEditorHeight] = useState(minHeight)
+ const [previousKeys, setPreviousKeys] = useState([])
+ const handleChange = (newTemplates: string, keys: string[]) => {
+ onChange?.(newTemplates)
+ }
+
+ return (
+ {value?.length || 0}
+
+ )}
+ ><>
+ {value !== undefined && ({
+ // id: item.id,
+ // name: item.name,
+ // type: item.data_source_type,
+ // })),
+ // onAddContext: ()=>{console.log('?onAddContext')},
+ }}
+ variableBlock={{
+ show: true,
+ variables:promptVariables?.map(x=>({name:x.key, value:x.key})) || [],
+ }}
+ externalToolBlock={{
+ show: false,
+ externalTools: [],
+ // onAddExternalTool: handleOpenExternalDataToolModal,
+ }}
+ historyBlock={{
+ show: false,
+ selectable: false,
+ history: {
+ user: '',
+ assistant: '',
+ },
+ onEditRole: () => { },
+ }}
+ queryBlock={{
+ show: false,
+ selectable: true,
+ }}
+ onChange={(value) => {
+ handleChange?.(value, [])
+ }}
+ onBlur={() => {
+ const keys = getVars(value)
+ handleChange(value, keys)
+ if(keys.filter(key => !previousKeys.includes(key)).length > 0){
+ variablesChange?.(keys)
+ setPreviousKeys(keys)
+ }
+ }}
+ editable={true}
+ />
+ }>
+ )
+}
+
+export default PromptEditorResizable
\ No newline at end of file
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/constants.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/constants.tsx
new file mode 100644
index 00000000..f7fd5204
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/constants.tsx
@@ -0,0 +1,52 @@
+// import type { ValueSelector } from '../../workflow/types'
+
+export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
+export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
+export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
+export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
+export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
+export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'
+export const MAX_VAR_KEY_LENGTH = 30
+
+export const checkHasContextBlock = (text: string) => {
+ if (!text)
+ return false
+ return text.includes(CONTEXT_PLACEHOLDER_TEXT)
+}
+
+export const checkHasHistoryBlock = (text: string) => {
+ if (!text)
+ return false
+ return text.includes(HISTORY_PLACEHOLDER_TEXT)
+}
+
+export const checkHasQueryBlock = (text: string) => {
+ if (!text)
+ return false
+ return text.includes(QUERY_PLACEHOLDER_TEXT)
+}
+
+/*
+* {{#1711617514996.name#}} => [1711617514996, name]
+* {{#1711617514996.sys.query#}} => [sys, query]
+*/
+export const getInputVars = (text: string) => {
+ if (!text)
+ return []
+
+ const allVars = text.match(/{{#([^#]*)#}}/g)
+ if (allVars && allVars?.length > 0) {
+ // {{#context#}}, {{#query#}} is not input vars
+ const inputVars = allVars
+ .filter(item => item.includes('.'))
+ .map((item) => {
+ const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.')
+ if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0]))
+ return valueSelector.slice(1)
+
+ return valueSelector
+ })
+ return inputVars
+ }
+ return []
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/hooks.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/hooks.ts
new file mode 100644
index 00000000..c9e4cc12
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/hooks.ts
@@ -0,0 +1,185 @@
+import {
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from 'react'
+import type { Dispatch, RefObject, SetStateAction } from 'react'
+import type {
+ Klass,
+ LexicalCommand,
+ LexicalEditor,
+ TextNode,
+} from 'lexical'
+import {
+ $getNodeByKey,
+ $getSelection,
+ $isDecoratorNode,
+ $isNodeSelection,
+ COMMAND_PRIORITY_LOW,
+ KEY_BACKSPACE_COMMAND,
+ KEY_DELETE_COMMAND,
+} from 'lexical'
+import type { EntityMatch } from '@lexical/text'
+import {
+ mergeRegister,
+} from '@lexical/utils'
+import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { $isContextBlockNode } from './plugins/context-block/node'
+import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block'
+import { $isHistoryBlockNode } from './plugins/history-block/node'
+import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block'
+import { $isQueryBlockNode } from './plugins/query-block/node'
+import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
+import type { CustomTextNode } from './plugins/custom-text/node'
+import { registerLexicalTextEntity } from './utils'
+
+export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand) => [RefObject, boolean]
+export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand) => {
+ const ref = useRef(null)
+ const [editor] = useLexicalComposerContext()
+ const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
+
+ const handleDelete = useCallback(
+ (event: KeyboardEvent) => {
+ const selection = $getSelection()
+ const nodes = selection?.getNodes()
+ if (
+ !isSelected
+ && nodes?.length === 1
+ && (
+ ($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND)
+ || ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND)
+ || ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
+ )
+ )
+ editor.dispatchCommand(command, undefined)
+
+ if (isSelected && $isNodeSelection(selection)) {
+ event.preventDefault()
+ const node = $getNodeByKey(nodeKey)
+ if ($isDecoratorNode(node)) {
+ if (command)
+ editor.dispatchCommand(command, undefined)
+
+ node.remove()
+ return true
+ }
+ }
+
+ return false
+ },
+ [isSelected, nodeKey, command, editor],
+ )
+
+ const handleSelect = useCallback((e: MouseEvent) => {
+ e.stopPropagation()
+ clearSelection()
+ setSelected(true)
+ }, [setSelected, clearSelection])
+
+ useEffect(() => {
+ const ele = ref.current
+
+ if (ele)
+ ele.addEventListener('click', handleSelect)
+
+ return () => {
+ if (ele)
+ ele.removeEventListener('click', handleSelect)
+ }
+ }, [handleSelect])
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerCommand(
+ KEY_DELETE_COMMAND,
+ handleDelete,
+ COMMAND_PRIORITY_LOW,
+ ),
+ editor.registerCommand(
+ KEY_BACKSPACE_COMMAND,
+ handleDelete,
+ COMMAND_PRIORITY_LOW,
+ ),
+ )
+ }, [editor, clearSelection, handleDelete])
+
+ return [ref, isSelected]
+}
+
+export type UseTriggerHandler = () => [RefObject, boolean, Dispatch>]
+export const useTrigger: UseTriggerHandler = () => {
+ const triggerRef = useRef(null)
+ const [open, setOpen] = useState(false)
+ const handleOpen = useCallback((e: MouseEvent) => {
+ e.stopPropagation()
+ setOpen(v => !v)
+ }, [])
+
+ useEffect(() => {
+ const trigger = triggerRef.current
+ if (trigger)
+ trigger.addEventListener('click', handleOpen)
+
+ return () => {
+ if (trigger)
+ trigger.removeEventListener('click', handleOpen)
+ }
+ }, [handleOpen])
+
+ return [triggerRef, open, setOpen]
+}
+
+export function useLexicalTextEntity(
+ getMatch: (text: string) => null | EntityMatch,
+ targetNode: Klass,
+ createNode: (textNode: CustomTextNode) => T,
+) {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode))
+ }, [createNode, editor, getMatch, targetNode])
+}
+
+export type MenuTextMatch = {
+ leadOffset: number
+ matchingString: string
+ replaceableString: string
+}
+export type TriggerFn = (
+ text: string,
+ editor: LexicalEditor,
+) => MenuTextMatch | null
+export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
+export function useBasicTypeaheadTriggerMatch(
+ trigger: string,
+ { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
+): TriggerFn {
+ return useCallback(
+ (text: string) => {
+ const validChars = `[${PUNCTUATION}\\s]`
+ const TypeaheadTriggerRegex = new RegExp(
+ '(.*)('
+ + `[${trigger}]`
+ + `((?:${validChars}){0,${maxLength}})`
+ + ')$',
+ )
+ const match = TypeaheadTriggerRegex.exec(text)
+ if (match !== null) {
+ const maybeLeadingWhitespace = match[1]
+ const matchingString = match[3]
+ if (matchingString.length >= minLength) {
+ return {
+ leadOffset: match.index + maybeLeadingWhitespace.length,
+ matchingString,
+ replaceableString: match[2],
+ }
+ }
+ }
+ return null
+ },
+ [maxLength, minLength, trigger],
+ )
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/hooks.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/hooks.tsx
new file mode 100644
index 00000000..e09a83ed
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/hooks.tsx
@@ -0,0 +1,294 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { $insertNodes } from 'lexical'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type {
+ ContextBlockType,
+ ExternalToolBlockType,
+ HistoryBlockType,
+ QueryBlockType,
+ VariableBlockType,
+ WorkflowVariableBlockType,
+} from '../../types'
+import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block'
+import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block'
+import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block'
+import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
+import { $createCustomTextNode } from '../custom-text/node'
+import { PromptMenuItem } from './prompt-option'
+import { VariableMenuItem } from './variable-option'
+import { PickerBlockMenuOption } from './menu'
+import { $t } from '@common/locales'
+// import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
+// import {
+// MessageClockCircle,
+// Tool03,
+// } from '@/app/components/base/icons/src/vender/solid/general'
+// import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
+// import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
+// import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
+// import AppIcon from '@/app/components/base/app-icon'
+
+export const usePromptOptions = (
+ contextBlock?: ContextBlockType,
+ queryBlock?: QueryBlockType,
+ historyBlock?: HistoryBlockType,
+) => {
+ const [editor] = useLexicalComposerContext()
+
+ const promptOptions: PickerBlockMenuOption[] = []
+ if (contextBlock?.show) {
+ promptOptions.push(new PickerBlockMenuOption({
+ key: $t('上下文'),
+ group: 'prompt context',
+ render: ({ isSelected, onSelect, onSetHighlight }) => {
+ return >}
+ // icon={}
+ disabled={!contextBlock.selectable}
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ },
+ onSelect: () => {
+ if (!contextBlock?.selectable)
+ return
+ editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
+ },
+ }))
+ }
+
+ if (queryBlock?.show) {
+ promptOptions.push(
+ new PickerBlockMenuOption({
+ key: $t('查询内容'),
+ group: 'prompt query',
+ render: ({ isSelected, onSelect, onSetHighlight }) => {
+ return (
+ >}
+ // icon={}
+ disabled={!queryBlock.selectable}
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ )
+ },
+ onSelect: () => {
+ if (!queryBlock?.selectable)
+ return
+ editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
+ },
+ }),
+ )
+ }
+
+ if (historyBlock?.show) {
+ promptOptions.push(
+ new PickerBlockMenuOption({
+ key: $t('会话历史'),
+ group: 'prompt history',
+ render: ({ isSelected, onSelect, onSetHighlight }) => {
+ return (
+ >}
+ // icon={}
+ disabled={!historyBlock.selectable
+ }
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ )
+ },
+ onSelect: () => {
+ if (!historyBlock?.selectable)
+ return
+ editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
+ },
+ }),
+ )
+ }
+ return promptOptions
+}
+
+export const useVariableOptions = (
+ variableBlock?: VariableBlockType,
+ queryString?: string,
+): PickerBlockMenuOption[] => {
+ const { t } = useTranslation()
+ const [editor] = useLexicalComposerContext()
+
+ const options = useMemo(() => {
+ if (!variableBlock?.variables)
+ return []
+
+ const baseOptions = (variableBlock.variables).map((item) => {
+ return new PickerBlockMenuOption({
+ key: item.value,
+ group: 'prompt variable',
+ render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
+ return (
+ >}
+ // icon={}
+ queryString={queryString}
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ )
+ },
+ onSelect: () => {
+ editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
+ },
+ })
+ })
+ if (!queryString)
+ return baseOptions
+
+ const regex = new RegExp(queryString, 'i')
+
+ return baseOptions.filter(option => regex.test(option.key))
+ }, [editor, queryString, variableBlock])
+
+ const addOption = useMemo(() => {
+ return new PickerBlockMenuOption({
+ key: $t('添加新变量'),
+ group: 'prompt variable',
+ render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
+ return (
+ >}
+ // icon={}
+ queryString={queryString}
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ )
+ },
+ onSelect: () => {
+ editor.update(() => {
+ const prefixNode = $createCustomTextNode('{{')
+ const suffixNode = $createCustomTextNode('}}')
+ $insertNodes([prefixNode, suffixNode])
+ prefixNode.select()
+ })
+ },
+ })
+ }, [editor, t])
+
+ return useMemo(() => {
+ return variableBlock?.show ? [...options, addOption] : []
+ }, [options, addOption, variableBlock?.show])
+}
+
+export const useExternalToolOptions = (
+ externalToolBlockType?: ExternalToolBlockType,
+ queryString?: string,
+) => {
+ const { t } = useTranslation()
+ const [editor] = useLexicalComposerContext()
+
+ const options = useMemo(() => {
+ if (!externalToolBlockType?.externalTools)
+ return []
+ const baseToolOptions = (externalToolBlockType.externalTools).map((item) => {
+ return new PickerBlockMenuOption({
+ key: item.name,
+ group: 'external tool',
+ render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
+ return (
+ >}
+ // icon={
+ //
+ // }
+ extraElement={{item.variableName}
}
+ queryString={queryString}
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ )
+ },
+ onSelect: () => {
+ editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
+ },
+ })
+ })
+ if (!queryString)
+ return baseToolOptions
+
+ const regex = new RegExp(queryString, 'i')
+
+ return baseToolOptions.filter(option => regex.test(option.key))
+ }, [editor, queryString, externalToolBlockType])
+
+ const addOption = useMemo(() => {
+ return new PickerBlockMenuOption({
+ key: $t('添加工具'),
+ group: 'external tool',
+ render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
+ return (
+ }
+ // extraElement={< ArrowUpRight className='w-3 h-3 text-gray-400' />}
+ queryString={queryString}
+ isSelected={isSelected}
+ onClick={onSelect}
+ onMouseEnter={onSetHighlight}
+ />
+ )
+ },
+ onSelect: () => {
+ externalToolBlockType?.onAddExternalTool?.()
+ },
+ })
+ }, [externalToolBlockType, t])
+
+ return useMemo(() => {
+ return externalToolBlockType?.show ? [...options, addOption] : []
+ }, [options, addOption, externalToolBlockType?.show])
+}
+
+export const useOptions = (
+ contextBlock?: ContextBlockType,
+ queryBlock?: QueryBlockType,
+ historyBlock?: HistoryBlockType,
+ variableBlock?: VariableBlockType,
+ externalToolBlockType?: ExternalToolBlockType,
+ workflowVariableBlockType?: WorkflowVariableBlockType,
+ queryString?: string,
+) => {
+ const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
+ const variableOptions = useVariableOptions(variableBlock, queryString)
+ const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
+ const workflowVariableOptions = useMemo(() => {
+ if (!workflowVariableBlockType?.show)
+ return []
+
+ return workflowVariableBlockType.variables || []
+ }, [workflowVariableBlockType])
+
+ return useMemo(() => {
+ return {
+ workflowVariableOptions,
+ allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
+ }
+ }, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/index.tsx
new file mode 100644
index 00000000..bfece5ba
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/index.tsx
@@ -0,0 +1,212 @@
+import {
+ Fragment,
+ memo,
+ useCallback,
+ useState,
+} from 'react'
+import ReactDOM from 'react-dom'
+import {
+ flip,
+ offset,
+ shift,
+ useFloating,
+} from '@floating-ui/react'
+import type { TextNode } from 'lexical'
+import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
+import type {
+ ContextBlockType,
+ ExternalToolBlockType,
+ HistoryBlockType,
+ QueryBlockType,
+ VariableBlockType,
+ WorkflowVariableBlockType,
+} from '../../types'
+import { useBasicTypeaheadTriggerMatch } from '../../hooks'
+import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
+import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
+import { $splitNodeContainingQuery } from '../../utils'
+import { useOptions } from './hooks'
+import type { PickerBlockMenuOption } from './menu'
+// import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
+import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
+
+type ComponentPickerProps = {
+ triggerString: string
+ contextBlock?: ContextBlockType
+ queryBlock?: QueryBlockType
+ historyBlock?: HistoryBlockType
+ variableBlock?: VariableBlockType
+ externalToolBlock?: ExternalToolBlockType
+ workflowVariableBlock?: WorkflowVariableBlockType
+}
+const ComponentPicker = ({
+ triggerString,
+ contextBlock,
+ queryBlock,
+ historyBlock,
+ variableBlock,
+ externalToolBlock,
+ workflowVariableBlock,
+}: ComponentPickerProps) => {
+ const { eventEmitter } = useEventEmitterContextContext()
+ const { refs, floatingStyles, isPositioned } = useFloating({
+ placement: 'bottom-start',
+ middleware: [
+ offset(0), // fix hide cursor
+ shift({
+ padding: 8,
+ }),
+ flip(),
+ ],
+ })
+ const [editor] = useLexicalComposerContext()
+ const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
+ minLength: 0,
+ maxLength: 0,
+ })
+
+ const [queryString, setQueryString] = useState(null)
+
+ eventEmitter?.useSubscription((v: any) => {
+ if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
+ editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
+ })
+
+ const {
+ allFlattenOptions,
+ workflowVariableOptions,
+ } = useOptions(
+ contextBlock,
+ queryBlock,
+ historyBlock,
+ variableBlock,
+ externalToolBlock,
+ workflowVariableBlock,
+ )
+
+ const onSelectOption = useCallback(
+ (
+ selectedOption: PickerBlockMenuOption,
+ nodeToRemove: TextNode | null,
+ closeMenu: () => void,
+ ) => {
+ editor.update(() => {
+ if (nodeToRemove && selectedOption?.key)
+ nodeToRemove.remove()
+
+ selectedOption.onSelectMenuOption()
+ closeMenu()
+ })
+ },
+ [editor],
+ )
+
+ const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
+ editor.update(() => {
+ const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
+ if (needRemove)
+ needRemove.remove()
+ })
+
+ if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
+ editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
+ else
+ editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
+ }, [editor, checkForTriggerMatch, triggerString])
+
+ const renderMenu = useCallback>((
+ anchorElementRef,
+ { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
+ ) => {
+ if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
+ return null
+ refs.setReference(anchorElementRef.current)
+
+ return (
+ <>
+ {
+ ReactDOM.createPortal(
+ // The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
+ // Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
+ // See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
+
+
+ {
+ options.map((option, index) => (
+
+ {
+ // Divider
+ index !== 0 && options.at(index - 1)?.group !== option.group && (
+
+ )
+ }
+ {option.renderMenuOption({
+ queryString,
+ isSelected: selectedIndex === index,
+ onSelect: () => {
+ selectOptionAndCleanUp(option)
+ },
+ onSetHighlight: () => {
+ setHighlightedIndex(index)
+ },
+ })}
+
+ ))
+ }
+ {
+ workflowVariableBlock?.show && (
+ <>
+ {
+ (!!options.length) && (
+
+ )
+ }
+
+ {/* {
+ handleSelectWorkflowVariable(variables)
+ }}
+ /> */}
+
+ >
+ )
+ }
+
+
,
+ anchorElementRef.current,
+ )
+ }
+ >
+ )
+ }, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable])
+
+ return (
+
+ )
+}
+
+export default memo(ComponentPicker)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/menu.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/menu.tsx
new file mode 100644
index 00000000..d8c71569
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/menu.tsx
@@ -0,0 +1,31 @@
+import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
+import { Fragment } from 'react'
+
+/**
+ * Corresponds to the `MenuRenderFn` type from `@lexical/react/LexicalTypeaheadMenuPlugin`.
+ */
+type MenuOptionRenderProps = {
+ isSelected: boolean
+ onSelect: () => void
+ onSetHighlight: () => void
+ queryString: string | null
+}
+
+export class PickerBlockMenuOption extends MenuOption {
+ public group?: string
+
+ constructor(
+ private data: {
+ key: string
+ group?: string
+ onSelect?: () => void
+ render: (menuRenderProps: MenuOptionRenderProps) => JSX.Element
+ },
+ ) {
+ super(data.key)
+ this.group = data.group
+ }
+
+ public onSelectMenuOption = () => this.data.onSelect?.()
+ public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => {this.data.render(menuRenderProps)}
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/prompt-option.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/prompt-option.tsx
new file mode 100644
index 00000000..7aabbe4b
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/prompt-option.tsx
@@ -0,0 +1,45 @@
+import { memo } from 'react'
+
+type PromptMenuItemMenuItemProps = {
+ icon: JSX.Element
+ title: string
+ disabled?: boolean
+ isSelected: boolean
+ onClick: () => void
+ onMouseEnter: () => void
+ setRefElement?: (element: HTMLDivElement) => void
+}
+export const PromptMenuItem = memo(({
+ icon,
+ title,
+ disabled,
+ isSelected,
+ onClick,
+ onMouseEnter,
+ setRefElement,
+}: PromptMenuItemMenuItemProps) => {
+ return (
+ {
+ if (disabled)
+ return
+ onMouseEnter()
+ }}
+ onClick={() => {
+ if (disabled)
+ return
+ onClick()
+ }}>
+ {icon}
+
{title}
+
+ )
+})
+PromptMenuItem.displayName = 'PromptMenuItem'
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/variable-option.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/variable-option.tsx
new file mode 100644
index 00000000..bb5290c8
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/component-picker-block/variable-option.tsx
@@ -0,0 +1,60 @@
+import { memo } from 'react'
+
+type VariableMenuItemProps = {
+ title: string
+ icon?: JSX.Element
+ extraElement?: JSX.Element
+ isSelected: boolean
+ queryString: string | null
+ onClick: () => void
+ onMouseEnter: () => void
+ setRefElement?: (element: HTMLDivElement) => void
+}
+export const VariableMenuItem = memo(({
+ title,
+ icon,
+ extraElement,
+ isSelected,
+ queryString,
+ onClick,
+ onMouseEnter,
+ setRefElement,
+}: VariableMenuItemProps) => {
+ let before = title
+ let middle = ''
+ let after = ''
+
+ if (queryString) {
+ const regex = new RegExp(queryString, 'i')
+ const match = regex.exec(title)
+
+ if (match) {
+ before = title.substring(0, match.index)
+ middle = match[0]
+ after = title.substring(match.index + match[0].length)
+ }
+ }
+
+ return (
+
+
+ {icon}
+
+
+ {before}
+ {middle}
+ {after}
+
+ {extraElement}
+
+ )
+})
+VariableMenuItem.displayName = 'VariableMenuItem'
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/component.tsx
new file mode 100644
index 00000000..60346493
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/component.tsx
@@ -0,0 +1,56 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+// import {
+// RiAddLine,
+// } from '@remixicon/react'
+import { useSelectOrDelete, useTrigger } from '../../hooks'
+import { UPDATE_DATASETS_EVENT_EMITTER } from '../../constants'
+import type { Dataset } from './index'
+import { DELETE_CONTEXT_BLOCK_COMMAND } from './index'
+// import { File05, Folder } from '@/app/components/base/icons/src/vender/solid/files'
+// import {
+// PortalToFollowElem,
+// PortalToFollowElemContent,
+// PortalToFollowElemTrigger,
+// } from '@/app/components/base/portal-to-follow-elem'
+import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
+import { $t } from '@common/locales'
+
+type ContextBlockComponentProps = {
+ nodeKey: string
+ datasets?: Dataset[]
+ onAddContext: () => void
+ canNotAddContext?: boolean
+}
+
+const ContextBlockComponent: FC = ({
+ nodeKey,
+ datasets = [],
+ onAddContext,
+ canNotAddContext,
+}) => {
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND)
+ const [triggerRef, open, setOpen] = useTrigger()
+ const { eventEmitter } = useEventEmitterContextContext()
+ const [localDatasets, setLocalDatasets] = useState(datasets)
+
+ eventEmitter?.useSubscription((v: any) => {
+ if (v?.type === UPDATE_DATASETS_EVENT_EMITTER)
+ setLocalDatasets(v.payload)
+ })
+
+ return (
+
+ {/*
*/}
+
{$t('上下文')}
+
+
+ )
+}
+
+export default ContextBlockComponent
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/context-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/context-block-replacement-block.tsx
new file mode 100644
index 00000000..7470edac
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/context-block-replacement-block.tsx
@@ -0,0 +1,63 @@
+import {
+ memo,
+ useCallback,
+ useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants'
+import type { ContextBlockType } from '../../types'
+import {
+ $createContextBlockNode,
+ ContextBlockNode,
+} from './node'
+import { CustomTextNode } from '../custom-text/node'
+
+const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
+
+const ContextBlockReplacementBlock = ({
+ datasets = [],
+ onAddContext = () => {},
+ onInsert,
+ canNotAddContext,
+}: ContextBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([ContextBlockNode]))
+ throw new Error('ContextBlockNodePlugin: ContextBlockNode not registered on editor')
+ }, [editor])
+
+ const createContextBlockNode = useCallback((): ContextBlockNode => {
+ if (onInsert)
+ onInsert()
+ return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext))
+ }, [datasets, onAddContext, onInsert, canNotAddContext])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
+ )
+ }, [])
+
+ return null
+}
+
+export default memo(ContextBlockReplacementBlock)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/index.tsx
new file mode 100644
index 00000000..5be4f1fa
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/index.tsx
@@ -0,0 +1,74 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { ContextBlockType } from '../../types'
+import {
+ $createContextBlockNode,
+ ContextBlockNode,
+} from './node'
+
+export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND')
+export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND')
+
+export type Dataset = {
+ id: string
+ name: string
+ type: string
+}
+
+const ContextBlock = memo(({
+ datasets = [],
+ onAddContext = () => {},
+ onInsert,
+ onDelete,
+ canNotAddContext,
+}: ContextBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([ContextBlockNode]))
+ throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_CONTEXT_BLOCK_COMMAND,
+ () => {
+ const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext)
+
+ $insertNodes([contextBlockNode])
+
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_CONTEXT_BLOCK_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
+
+ return null
+})
+ContextBlock.displayName = 'ContextBlock'
+
+export { ContextBlock }
+export { ContextBlockNode } from './node'
+export { default as ContextBlockReplacementBlock } from './context-block-replacement-block'
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/node.tsx
new file mode 100644
index 00000000..3800b9bb
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/context-block/node.tsx
@@ -0,0 +1,100 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import ContextBlockComponent from './component'
+import type { Dataset } from './index'
+
+export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void; canNotAddContext: boolean }
+
+export class ContextBlockNode extends DecoratorNode {
+ __datasets: Dataset[]
+ __onAddContext: () => void
+ __canNotAddContext: boolean
+
+ static getType(): string {
+ return 'context-block'
+ }
+
+ static clone(node: ContextBlockNode): ContextBlockNode {
+ return new ContextBlockNode(node.__datasets, node.__onAddContext, node.getKey(), node.__canNotAddContext)
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey, canNotAddContext?: boolean) {
+ super(key)
+
+ this.__datasets = datasets
+ this.__onAddContext = onAddContext
+ this.__canNotAddContext = canNotAddContext || false
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): JSX.Element {
+ return (
+
+ )
+ }
+
+ getDatasets(): Dataset[] {
+ const self = this.getLatest()
+
+ return self.__datasets
+ }
+
+ getOnAddContext(): () => void {
+ const self = this.getLatest()
+
+ return self.__onAddContext
+ }
+
+ getCanNotAddContext(): boolean {
+ const self = this.getLatest()
+
+ return self.__canNotAddContext
+ }
+
+ static importJSON(serializedNode: SerializedNode): ContextBlockNode {
+ const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext, serializedNode.canNotAddContext)
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'context-block',
+ version: 1,
+ datasets: this.getDatasets(),
+ onAddContext: this.getOnAddContext(),
+ canNotAddContext: this.getCanNotAddContext(),
+ }
+ }
+
+ getTextContent(): string {
+ return '{{#context#}}'
+ }
+}
+export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void, canNotAddContext?: boolean): ContextBlockNode {
+ return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext)
+}
+
+export function $isContextBlockNode(
+ node: ContextBlockNode | LexicalNode | null | undefined,
+): boolean {
+ return node instanceof ContextBlockNode
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/custom-text/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/custom-text/node.tsx
new file mode 100644
index 00000000..5df4894c
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/custom-text/node.tsx
@@ -0,0 +1,52 @@
+import type { EditorConfig, NodeKey, SerializedTextNode } from 'lexical'
+import { $createTextNode, TextNode } from 'lexical'
+
+export class CustomTextNode extends TextNode {
+ static getType() {
+ return 'custom-text'
+ }
+
+ static clone(node: CustomTextNode) {
+ return new CustomTextNode(node.__text, node.__key)
+ }
+
+ constructor(text: string, key?: NodeKey) {
+ super(text, key)
+ }
+
+ createDOM(config: EditorConfig) {
+ const dom = super.createDOM(config)
+ dom.classList.add('align-middle')
+ return dom
+ }
+
+ static importJSON(serializedNode: SerializedTextNode): TextNode {
+ const node = $createTextNode(serializedNode.text)
+ node.setFormat(serializedNode.format)
+ node.setDetail(serializedNode.detail)
+ node.setMode(serializedNode.mode)
+ node.setStyle(serializedNode.style)
+ return node
+ }
+
+ exportJSON(): SerializedTextNode {
+ return {
+ detail: this.getDetail(),
+ format: this.getFormat(),
+ mode: this.getMode(),
+ style: this.getStyle(),
+ text: this.getTextContent(),
+ type: 'custom-text',
+ version: 1,
+ }
+ }
+
+ isSimpleText() {
+ return (
+ (this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0)
+ }
+}
+
+export function $createCustomTextNode(text: string): CustomTextNode {
+ return new CustomTextNode(text)
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/component.tsx
new file mode 100644
index 00000000..54e0cc5a
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/component.tsx
@@ -0,0 +1,53 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+// import {
+// RiMoreFill,
+// } from '@remixicon/react'
+import { useSelectOrDelete, useTrigger } from '../../hooks'
+import { UPDATE_HISTORY_EVENT_EMITTER } from '../../constants'
+import type { RoleName } from './index'
+import { DELETE_HISTORY_BLOCK_COMMAND } from './index'
+// import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
+// import {
+// PortalToFollowElem,
+// PortalToFollowElemContent,
+// PortalToFollowElemTrigger,
+// } from '@/app/components/base/portal-to-follow-elem'
+import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
+import { $t } from '@common/locales'
+
+type HistoryBlockComponentProps = {
+ nodeKey: string
+ roleName?: RoleName
+ onEditRole: () => void
+}
+
+const HistoryBlockComponent: FC = ({
+ nodeKey,
+ roleName = { user: '', assistant: '' },
+ onEditRole,
+}) => {
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND)
+ const [triggerRef, open, setOpen] = useTrigger()
+ const { eventEmitter } = useEventEmitterContextContext()
+ const [localRoleName, setLocalRoleName] = useState(roleName)
+
+ eventEmitter?.useSubscription((v: any) => {
+ if (v?.type === UPDATE_HISTORY_EVENT_EMITTER)
+ setLocalRoleName(v.payload)
+ })
+
+ return (
+
+ {/*
*/}
+
{$t('会话历史')}
+
+ )
+}
+
+export default HistoryBlockComponent
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/history-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/history-block-replacement-block.tsx
new file mode 100644
index 00000000..414d027c
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/history-block-replacement-block.tsx
@@ -0,0 +1,61 @@
+import {
+ useCallback,
+ useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import { HISTORY_PLACEHOLDER_TEXT } from '../../constants'
+import type { HistoryBlockType } from '../../types'
+import {
+ $createHistoryBlockNode,
+ HistoryBlockNode,
+} from './node'
+import { CustomTextNode } from '../custom-text/node'
+
+const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
+
+const HistoryBlockReplacementBlock = ({
+ history = { user: '', assistant: '' },
+ onEditRole = () => {},
+ onInsert,
+}: HistoryBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([HistoryBlockNode]))
+ throw new Error('HistoryBlockNodePlugin: HistoryBlockNode not registered on editor')
+ }, [editor])
+
+ const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
+ if (onInsert)
+ onInsert()
+ return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole))
+ }, [history, onEditRole, onInsert])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
+ )
+ }, [])
+
+ return null
+}
+
+export default HistoryBlockReplacementBlock
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/index.tsx
new file mode 100644
index 00000000..78c73495
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/index.tsx
@@ -0,0 +1,79 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { HistoryBlockType } from '../../types'
+import {
+ $createHistoryBlockNode,
+ HistoryBlockNode,
+} from './node'
+
+export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND')
+export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND')
+
+export type RoleName = {
+ user: string
+ assistant: string
+}
+
+export type HistoryBlockProps = {
+ roleName: RoleName
+ onEditRole: () => void
+ onInsert?: () => void
+ onDelete?: () => void
+}
+
+const HistoryBlock = memo(({
+ history = { user: '', assistant: '' },
+ onEditRole = () => {},
+ onInsert,
+ onDelete,
+}: HistoryBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([HistoryBlockNode]))
+ throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_HISTORY_BLOCK_COMMAND,
+ () => {
+ const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
+
+ $insertNodes([historyBlockNode])
+
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_HISTORY_BLOCK_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, history, onEditRole, onInsert, onDelete])
+
+ return null
+})
+HistoryBlock.displayName = 'HistoryBlock'
+
+export { HistoryBlock }
+export { HistoryBlockNode } from './node'
+export { default as HistoryBlockReplacementBlock } from './history-block-replacement-block'
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/node.tsx
new file mode 100644
index 00000000..4112b736
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/history-block/node.tsx
@@ -0,0 +1,90 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import HistoryBlockComponent from './component'
+import type { RoleName } from './index'
+
+export type SerializedNode = SerializedLexicalNode & { roleName: RoleName; onEditRole: () => void }
+
+export class HistoryBlockNode extends DecoratorNode {
+ __roleName: RoleName
+ __onEditRole: () => void
+
+ static getType(): string {
+ return 'history-block'
+ }
+
+ static clone(node: HistoryBlockNode): HistoryBlockNode {
+ return new HistoryBlockNode(node.__roleName, node.__onEditRole)
+ }
+
+ constructor(roleName: RoleName, onEditRole: () => void, key?: NodeKey) {
+ super(key)
+
+ this.__roleName = roleName
+ this.__onEditRole = onEditRole
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): JSX.Element {
+ return (
+
+ )
+ }
+
+ getRoleName(): RoleName {
+ const self = this.getLatest()
+
+ return self.__roleName
+ }
+
+ getOnEditRole(): () => void {
+ const self = this.getLatest()
+
+ return self.__onEditRole
+ }
+
+ static importJSON(serializedNode: SerializedNode): HistoryBlockNode {
+ const node = $createHistoryBlockNode(serializedNode.roleName, serializedNode.onEditRole)
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'history-block',
+ version: 1,
+ roleName: this.getRoleName(),
+ onEditRole: this.getOnEditRole,
+ }
+ }
+
+ getTextContent(): string {
+ return '{{#histories#}}'
+ }
+}
+export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => void): HistoryBlockNode {
+ return new HistoryBlockNode(roleName, onEditRole)
+}
+
+export function $isHistoryBlockNode(
+ node: HistoryBlockNode | LexicalNode | null | undefined,
+): node is HistoryBlockNode {
+ return node instanceof HistoryBlockNode
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/on-blur-or-focus-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/on-blur-or-focus-block.tsx
new file mode 100644
index 00000000..2e3adc15
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/on-blur-or-focus-block.tsx
@@ -0,0 +1,67 @@
+import type { FC } from 'react'
+import { useEffect, useRef } from 'react'
+import {
+ BLUR_COMMAND,
+ COMMAND_PRIORITY_EDITOR,
+ FOCUS_COMMAND,
+ KEY_ESCAPE_COMMAND,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
+
+type OnBlurBlockProps = {
+ onBlur?: () => void
+ onFocus?: () => void
+}
+const OnBlurBlock: FC = ({
+ onBlur,
+ onFocus,
+}) => {
+ const [editor] = useLexicalComposerContext()
+
+ const ref = useRef(null)
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerCommand(
+ CLEAR_HIDE_MENU_TIMEOUT,
+ () => {
+ if (ref.current) {
+ clearTimeout(ref.current)
+ ref.current = null
+ }
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ BLUR_COMMAND,
+ () => {
+ ref.current = setTimeout(() => {
+ editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
+ }, 200)
+
+ if (onBlur)
+ onBlur()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ FOCUS_COMMAND,
+ () => {
+ if (onFocus)
+ onFocus()
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, onBlur, onFocus])
+
+ return null
+}
+
+export default OnBlurBlock
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/placeholder.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/placeholder.tsx
new file mode 100644
index 00000000..d0ed3c9a
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/placeholder.tsx
@@ -0,0 +1,22 @@
+import { $t } from '@common/locales'
+import { memo } from 'react'
+
+const Placeholder = ({
+ compact,
+ value
+}: {
+ compact?: boolean
+ value?: string
+ className?: string
+}) => {
+
+ return (
+
+ {value || $t('AI 模型调用默认仅使用 Query 变量,可输入 “{” 增加新变量。')}
+
+ )
+}
+
+export default memo(Placeholder)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/component.tsx
new file mode 100644
index 00000000..115e51f1
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/component.tsx
@@ -0,0 +1,33 @@
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useSelectOrDelete } from '../../hooks'
+import { DELETE_QUERY_BLOCK_COMMAND } from './index'
+import { $t } from '@common/locales'
+// import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
+
+type QueryBlockComponentProps = {
+ nodeKey: string
+}
+
+const QueryBlockComponent: FC = ({
+ nodeKey,
+}) => {
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
+
+ return (
+
+ {/*
*/}
+
{'{{'}
+
{$t('查询内容')}
+
{'}}'}
+
+ )
+}
+
+export default QueryBlockComponent
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/index.tsx
new file mode 100644
index 00000000..09461531
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/index.tsx
@@ -0,0 +1,68 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { QueryBlockType } from '../../types'
+import {
+ $createQueryBlockNode,
+ QueryBlockNode,
+} from './node'
+
+export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND')
+export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND')
+
+export type QueryBlockProps = {
+ onInsert?: () => void
+ onDelete?: () => void
+}
+const QueryBlock = memo(({
+ onInsert,
+ onDelete,
+}: QueryBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([QueryBlockNode]))
+ throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_QUERY_BLOCK_COMMAND,
+ () => {
+ const contextBlockNode = $createQueryBlockNode()
+
+ $insertNodes([contextBlockNode])
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_QUERY_BLOCK_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, onInsert, onDelete])
+
+ return null
+})
+QueryBlock.displayName = 'QueryBlock'
+
+export { QueryBlock }
+export { QueryBlockNode } from './node'
+export { default as QueryBlockReplacementBlock } from './query-block-replacement-block'
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/node.tsx
new file mode 100644
index 00000000..3b5f6c0d
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/node.tsx
@@ -0,0 +1,59 @@
+import type { LexicalNode, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import QueryBlockComponent from './component'
+
+export type SerializedNode = SerializedLexicalNode
+
+export class QueryBlockNode extends DecoratorNode {
+ static getType(): string {
+ return 'query-block'
+ }
+
+ static clone(): QueryBlockNode {
+ return new QueryBlockNode()
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): JSX.Element {
+ return
+ }
+
+ static importJSON(): QueryBlockNode {
+ const node = $createQueryBlockNode()
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'query-block',
+ version: 1,
+ }
+ }
+
+ getTextContent(): string {
+ return '{{#query#}}'
+ }
+}
+export function $createQueryBlockNode(): QueryBlockNode {
+ return new QueryBlockNode()
+}
+
+export function $isQueryBlockNode(
+ node: QueryBlockNode | LexicalNode | null | undefined,
+): node is QueryBlockNode {
+ return node instanceof QueryBlockNode
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/query-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/query-block-replacement-block.tsx
new file mode 100644
index 00000000..0959f93c
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/query-block/query-block-replacement-block.tsx
@@ -0,0 +1,60 @@
+import {
+ memo,
+ useCallback,
+ useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import { QUERY_PLACEHOLDER_TEXT } from '../../constants'
+import type { QueryBlockType } from '../../types'
+import {
+ $createQueryBlockNode,
+ QueryBlockNode,
+} from './node'
+import { CustomTextNode } from '../custom-text/node'
+
+const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
+
+const QueryBlockReplacementBlock = ({
+ onInsert,
+}: QueryBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([QueryBlockNode]))
+ throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor')
+ }, [editor])
+
+ const createQueryBlockNode = useCallback((): QueryBlockNode => {
+ if (onInsert)
+ onInsert()
+ return $applyNodeReplacement($createQueryBlockNode())
+ }, [onInsert])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
+ )
+ }, [])
+
+ return null
+}
+
+export default memo(QueryBlockReplacementBlock)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/tree-view.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/tree-view.tsx
new file mode 100644
index 00000000..29028cb2
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/tree-view.tsx
@@ -0,0 +1,19 @@
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { TreeView } from '@lexical/react/LexicalTreeView'
+
+const TreeViewPlugin = () => {
+ const [editor] = useLexicalComposerContext()
+ return (
+
+ )
+}
+
+export default TreeViewPlugin
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/update-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/update-block.tsx
new file mode 100644
index 00000000..ad5c50dd
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/update-block.tsx
@@ -0,0 +1,42 @@
+import { $insertNodes } from 'lexical'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { textToEditorState } from '../utils'
+import { CustomTextNode } from './custom-text/node'
+import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
+import { useEventEmitterContextContext } from '@common/contexts/event-emitter'
+
+export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
+export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
+
+type UpdateBlockProps = {
+ instanceId?: string
+}
+const UpdateBlock = ({
+ instanceId,
+}: UpdateBlockProps) => {
+ const { eventEmitter } = useEventEmitterContextContext()
+ const [editor] = useLexicalComposerContext()
+
+ eventEmitter?.useSubscription((v: any) => {
+ if (v.type === PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER && v.instanceId === instanceId) {
+ const editorState = editor.parseEditorState(textToEditorState(v.payload))
+ editor.setEditorState(editorState)
+ }
+ })
+
+ eventEmitter?.useSubscription((v: any) => {
+ if (v.type === PROMPT_EDITOR_INSERT_QUICKLY && v.instanceId === instanceId) {
+ editor.focus()
+ editor.update(() => {
+ const textNode = new CustomTextNode('/')
+ $insertNodes([textNode])
+
+ editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
+ })
+ }
+ })
+
+ return null
+}
+
+export default UpdateBlock
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-block/index.tsx
new file mode 100644
index 00000000..3c995d59
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-block/index.tsx
@@ -0,0 +1,45 @@
+import { useEffect } from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { CustomTextNode } from '../custom-text/node'
+
+export const INSERT_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_BLOCK_COMMAND')
+export const INSERT_VARIABLE_VALUE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_VALUE_BLOCK_COMMAND')
+
+const VariableBlock = () => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_VARIABLE_BLOCK_COMMAND,
+ () => {
+ const textNode = new CustomTextNode('{')
+ $insertNodes([textNode])
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
+ (value: string) => {
+ const textNode = new CustomTextNode(value)
+ $insertNodes([textNode])
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor])
+
+ return null
+}
+
+export default VariableBlock
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/index.tsx
new file mode 100644
index 00000000..e93c0d7f
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/index.tsx
@@ -0,0 +1,52 @@
+import {
+ useCallback,
+ useEffect,
+} from 'react'
+import type { TextNode } from 'lexical'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useLexicalTextEntity } from '../../hooks'
+import {
+ $createVariableValueBlockNode,
+ VariableValueBlockNode,
+} from './node'
+import { getHashtagRegexString } from './utils'
+
+const REGEX = new RegExp(getHashtagRegexString(), 'i')
+
+const VariableValueBlock = () => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([VariableValueBlockNode]))
+ throw new Error('VariableValueBlockPlugin: VariableValueNode not registered on editor')
+ }, [editor])
+
+ const createVariableValueBlockNode = useCallback((textNode: TextNode): VariableValueBlockNode => {
+ return $createVariableValueBlockNode(textNode.getTextContent())
+ }, [])
+
+ const getVariableValueMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const hashtagLength = matchArr[0].length
+ const startOffset = matchArr.index
+ const endOffset = startOffset + hashtagLength
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ useLexicalTextEntity(
+ getVariableValueMatch,
+ VariableValueBlockNode,
+ createVariableValueBlockNode,
+ )
+
+ return null
+}
+
+export default VariableValueBlock
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/node.tsx
new file mode 100644
index 00000000..163d4bfa
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/node.tsx
@@ -0,0 +1,65 @@
+import type {
+ EditorConfig,
+ LexicalNode,
+ NodeKey,
+ SerializedTextNode,
+} from 'lexical'
+import {
+ $applyNodeReplacement,
+ TextNode,
+} from 'lexical'
+
+export class VariableValueBlockNode extends TextNode {
+ static getType(): string {
+ return 'variable-value-block'
+ }
+
+ static clone(node: VariableValueBlockNode): VariableValueBlockNode {
+ return new VariableValueBlockNode(node.__text, node.__key)
+ }
+
+ constructor(text: string, key?: NodeKey) {
+ super(text, key)
+ }
+
+ createDOM(config: EditorConfig): HTMLElement {
+ const element = super.createDOM(config)
+ element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-[#155EEF]', 'rounded-[5px]', 'align-middle')
+ return element
+ }
+
+ static importJSON(serializedNode: SerializedTextNode): TextNode {
+ const node = $createVariableValueBlockNode(serializedNode.text)
+ node.setFormat(serializedNode.format)
+ node.setDetail(serializedNode.detail)
+ node.setMode(serializedNode.mode)
+ node.setStyle(serializedNode.style)
+ return node
+ }
+
+ exportJSON(): SerializedTextNode {
+ return {
+ detail: this.getDetail(),
+ format: this.getFormat(),
+ mode: this.getMode(),
+ style: this.getStyle(),
+ text: this.getTextContent(),
+ type: 'variable-value-block',
+ version: 1,
+ }
+ }
+
+ canInsertTextBefore(): boolean {
+ return false
+ }
+}
+
+export function $createVariableValueBlockNode(text = ''): VariableValueBlockNode {
+ return $applyNodeReplacement(new VariableValueBlockNode(text))
+}
+
+export function $isVariableValueNodeBlock(
+ node: LexicalNode | null | undefined,
+): node is VariableValueBlockNode {
+ return node instanceof VariableValueBlockNode
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/utils.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/utils.ts
new file mode 100644
index 00000000..4d59d410
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/variable-value-block/utils.ts
@@ -0,0 +1,5 @@
+export function getHashtagRegexString(): string {
+ const hashtag = '\\{\\{[a-zA-Z_][a-zA-Z0-9_]{0,29}\\}\\}'
+
+ return hashtag
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/component.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/component.tsx
new file mode 100644
index 00000000..51017894
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/component.tsx
@@ -0,0 +1,114 @@
+import {
+ memo,
+ useEffect,
+ useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+ COMMAND_PRIORITY_EDITOR,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+// import {
+// RiErrorWarningFill,
+// } from '@remixicon/react'
+import { useSelectOrDelete } from '../../hooks'
+import type { WorkflowNodesMap } from './node'
+import { WorkflowVariableBlockNode } from './node'
+import {
+ DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
+ UPDATE_WORKFLOW_NODES_MAP,
+} from './index'
+// import cn from '@/utils/classnames'
+// import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
+// import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
+// import { VarBlockIcon } from '@/app/components/workflow/block-icon'
+// import { Line3 } from '@/app/components/base/icons/src/public/common'
+// import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
+// import Tooltip from '@/app/components/base/tooltip'
+
+type WorkflowVariableBlockComponentProps = {
+ nodeKey: string
+ variables: string[]
+ workflowNodesMap: WorkflowNodesMap
+}
+
+const WorkflowVariableBlockComponent = ({
+ nodeKey,
+ variables,
+ workflowNodesMap = {},
+}: WorkflowVariableBlockComponentProps) => {
+ const { t } = useTranslation()
+ const [editor] = useLexicalComposerContext()
+ const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND)
+ const variablesLength = variables.length
+ // const varName = (
+ // () => {
+ // const isSystem = isSystemVar(variables)
+ // const varName = variablesLength >= 3 ? (variables).slice(-2).join('.') : variables[variablesLength - 1]
+ // return `${isSystem ? 'sys.' : ''}${varName}`
+ // }
+ // )()
+ const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState(workflowNodesMap)
+ const node = localWorkflowNodesMap![variables[0]]
+ // const isEnv = isENV(variables)
+ // const isChatVar = isConversationVar(variables)
+
+ useEffect(() => {
+ if (!editor.hasNodes([WorkflowVariableBlockNode]))
+ throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ UPDATE_WORKFLOW_NODES_MAP,
+ (workflowNodesMap: WorkflowNodesMap) => {
+ setLocalWorkflowNodesMap(workflowNodesMap)
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor])
+
+ const Item = (
+
+ {/* {!isEnv && !isChatVar && (
+
+ {
+ node?.type && (
+
+
+
+ )
+ }
+
{node?.title}
+
+
+ )} */}
+
+ {/* {!isEnv && !isChatVar &&
}
+ {isEnv &&
}
+ {isChatVar &&
}
+
{varName}
+ {
+ !node && !isEnv && !isChatVar && (
+
+ )
+ } */}
+
+
+ )
+
+
+ return Item
+}
+
+export default memo(WorkflowVariableBlockComponent)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/index.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/index.tsx
new file mode 100644
index 00000000..8f0e2f48
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/index.tsx
@@ -0,0 +1,80 @@
+import {
+ memo,
+ useEffect,
+} from 'react'
+import {
+ $insertNodes,
+ COMMAND_PRIORITY_EDITOR,
+ createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import type { WorkflowVariableBlockType } from '../../types'
+import {
+ $createWorkflowVariableBlockNode,
+ WorkflowVariableBlockNode,
+} from './node'
+// import type { Node } from '@/app/components/workflow/types'
+
+export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
+export const DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND')
+export const CLEAR_HIDE_MENU_TIMEOUT = createCommand('CLEAR_HIDE_MENU_TIMEOUT')
+export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP')
+
+export type WorkflowVariableBlockProps = {
+ getWorkflowNode: (nodeId: string) => Node
+ onInsert?: () => void
+ onDelete?: () => void
+}
+const WorkflowVariableBlock = memo(({
+ workflowNodesMap,
+ onInsert,
+ onDelete,
+}: WorkflowVariableBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ editor.update(() => {
+ editor.dispatchCommand(UPDATE_WORKFLOW_NODES_MAP, workflowNodesMap)
+ })
+ }, [editor, workflowNodesMap])
+
+ useEffect(() => {
+ if (!editor.hasNodes([WorkflowVariableBlockNode]))
+ throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
+
+ return mergeRegister(
+ editor.registerCommand(
+ INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
+ (variables: string[]) => {
+ editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
+ const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap)
+
+ $insertNodes([workflowVariableBlockNode])
+ if (onInsert)
+ onInsert()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ editor.registerCommand(
+ DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
+ () => {
+ if (onDelete)
+ onDelete()
+
+ return true
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+ )
+ }, [editor, onInsert, onDelete, workflowNodesMap])
+
+ return null
+})
+WorkflowVariableBlock.displayName = 'WorkflowVariableBlock'
+
+export { WorkflowVariableBlock }
+export { WorkflowVariableBlockNode } from './node'
+export { default as WorkflowVariableBlockReplacementBlock } from './workflow-variable-block-replacement-block'
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/node.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/node.tsx
new file mode 100644
index 00000000..e4154731
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/node.tsx
@@ -0,0 +1,92 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import type { WorkflowVariableBlockType } from '../../types'
+import WorkflowVariableBlockComponent from './component'
+
+export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap']
+export type SerializedNode = SerializedLexicalNode & {
+ variables: string[]
+ workflowNodesMap: WorkflowNodesMap
+}
+
+export class WorkflowVariableBlockNode extends DecoratorNode {
+ __variables: string[]
+ __workflowNodesMap: WorkflowNodesMap
+
+ static getType(): string {
+ return 'workflow-variable-block'
+ }
+
+ static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
+ return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap)
+ }
+
+ isInline(): boolean {
+ return true
+ }
+
+ constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, key?: NodeKey) {
+ super(key)
+
+ this.__variables = variables
+ this.__workflowNodesMap = workflowNodesMap
+ }
+
+ createDOM(): HTMLElement {
+ const div = document.createElement('div')
+ div.classList.add('inline-flex', 'items-center', 'align-middle')
+ return div
+ }
+
+ updateDOM(): false {
+ return false
+ }
+
+ decorate(): JSX.Element {
+ return (
+
+ )
+ }
+
+ static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode {
+ const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap)
+
+ return node
+ }
+
+ exportJSON(): SerializedNode {
+ return {
+ type: 'workflow-variable-block',
+ version: 1,
+ variables: this.getVariables(),
+ workflowNodesMap: this.getWorkflowNodesMap(),
+ }
+ }
+
+ getVariables(): string[] {
+ const self = this.getLatest()
+ return self.__variables
+ }
+
+ getWorkflowNodesMap(): WorkflowNodesMap {
+ const self = this.getLatest()
+ return self.__workflowNodesMap
+ }
+
+ getTextContent(): string {
+ return `{{#${this.getVariables().join('.')}#}}`
+ }
+}
+export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode {
+ return new WorkflowVariableBlockNode(variables, workflowNodesMap)
+}
+
+export function $isWorkflowVariableBlockNode(
+ node: WorkflowVariableBlockNode | LexicalNode | null | undefined,
+): node is WorkflowVariableBlockNode {
+ return node instanceof WorkflowVariableBlockNode
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx
new file mode 100644
index 00000000..4571c18b
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx
@@ -0,0 +1,66 @@
+import {
+ memo,
+ useCallback,
+ useEffect,
+} from 'react'
+import type { TextNode } from 'lexical'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../../utils'
+import type { WorkflowVariableBlockType } from '../../types'
+import { CustomTextNode } from '../custom-text/node'
+import { $createWorkflowVariableBlockNode } from './node'
+import { WorkflowVariableBlockNode } from './index'
+// import { VAR_REGEX as REGEX } from '@/config'
+
+export const REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi
+
+const WorkflowVariableBlockReplacementBlock = ({
+ workflowNodesMap,
+ onInsert,
+}: WorkflowVariableBlockType) => {
+ const [editor] = useLexicalComposerContext()
+
+ useEffect(() => {
+ if (!editor.hasNodes([WorkflowVariableBlockNode]))
+ throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor')
+ }, [editor])
+
+ const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => {
+ if (onInsert)
+ onInsert()
+
+ const nodePathString = textNode.getTextContent().slice(3, -3)
+ return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap))
+ }, [onInsert, workflowNodesMap])
+
+ const getMatch = useCallback((text: string) => {
+ const matchArr = REGEX.exec(text)
+
+ if (matchArr === null)
+ return null
+
+ const startOffset = matchArr.index
+ const endOffset = startOffset + matchArr[0].length
+ return {
+ end: endOffset,
+ start: startOffset,
+ }
+ }, [])
+
+ const transformListener = useCallback((textNode: any) => {
+ return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
+ }, [createWorkflowVariableBlockNode, getMatch])
+
+ useEffect(() => {
+ REGEX.lastIndex = 0
+ return mergeRegister(
+ editor.registerNodeTransform(CustomTextNode, transformListener),
+ )
+ }, [])
+
+ return null
+}
+
+export default memo(WorkflowVariableBlockReplacementBlock)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx b/frontend/packages/common/src/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx
new file mode 100644
index 00000000..2376fb01
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx
@@ -0,0 +1,94 @@
+'use client'
+import React, { useCallback, useEffect, useState } from 'react'
+import type { FC } from 'react'
+import { useDebounceFn } from 'ahooks'
+
+type Props = {
+ className?: string
+ height: number
+ minHeight: number
+ onHeightChange: (height: number) => void
+ children: JSX.Element
+ footer?: JSX.Element
+ hideResize?: boolean
+}
+
+const PromptEditorHeightResizeWrap: FC = ({
+ className,
+ height,
+ minHeight,
+ onHeightChange,
+ children,
+ footer,
+ hideResize,
+}) => {
+ const [clientY, setClientY] = useState(0)
+ const [isResizing, setIsResizing] = useState(false)
+ const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)
+
+ const handleStartResize = useCallback((e: React.MouseEvent) => {
+ setClientY(e.clientY)
+ setIsResizing(true)
+ setPrevUserSelectStyle(getComputedStyle(document.body).userSelect)
+ document.body.style.userSelect = 'none'
+ }, [])
+
+ const handleStopResize = useCallback(() => {
+ setIsResizing(false)
+ document.body.style.userSelect = prevUserSelectStyle
+ }, [prevUserSelectStyle])
+
+ const { run: didHandleResize } = useDebounceFn((e) => {
+ if (!isResizing)
+ return
+
+ const offset = e.clientY - clientY
+ let newHeight = height + offset
+ setClientY(e.clientY)
+ if (newHeight < minHeight)
+ newHeight = minHeight
+ onHeightChange(newHeight)
+ }, {
+ wait: 0,
+ })
+
+ const handleResize = useCallback(didHandleResize, [isResizing, height, minHeight, clientY])
+
+ useEffect(() => {
+ document.addEventListener('mousemove', handleResize)
+ return () => {
+ document.removeEventListener('mousemove', handleResize)
+ }
+ }, [handleResize])
+
+ useEffect(() => {
+ document.addEventListener('mouseup', handleStopResize)
+ return () => {
+ document.removeEventListener('mouseup', handleStopResize)
+ }
+ }, [handleStopResize])
+
+ return (
+
+
+ {children}
+
+ {/* resize handler */}
+ {footer}
+ {!hideResize && (
+
+ )}
+
+ )
+}
+export default React.memo(PromptEditorHeightResizeWrap)
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/types.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/types.ts
new file mode 100644
index 00000000..8b7df07f
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/types.ts
@@ -0,0 +1,77 @@
+import type { Dataset } from './plugins/context-block/index'
+import type { RoleName } from './plugins/history-block/index'
+// import type {
+// Node,
+// } from '@/app/components/workflow/types'
+
+
+export type NodeOutPutVar = {
+ nodeId: string
+ title: string
+ vars: Var[]
+ isStartNode?: boolean
+}
+
+
+export type Option = {
+ value: string
+ name: string
+}
+
+export type ExternalToolOption = {
+ name: string
+ variableName: string
+ icon?: string
+ icon_background?: string
+}
+
+export type ContextBlockType = {
+ show?: boolean
+ selectable?: boolean
+ datasets?: Dataset[]
+ canNotAddContext?: boolean
+ onAddContext?: () => void
+ onInsert?: () => void
+ onDelete?: () => void
+}
+
+export type QueryBlockType = {
+ show?: boolean
+ selectable?: boolean
+ onInsert?: () => void
+ onDelete?: () => void
+}
+
+export type HistoryBlockType = {
+ show?: boolean
+ selectable?: boolean
+ history?: RoleName
+ onInsert?: () => void
+ onDelete?: () => void
+ onEditRole?: () => void
+}
+
+export type VariableBlockType = {
+ show?: boolean
+ variables?: Option[]
+}
+
+export type ExternalToolBlockType = {
+ show?: boolean
+ externalTools?: ExternalToolOption[]
+ onAddExternalTool?: () => void
+}
+unknown
+export type WorkflowVariableBlockType = {
+ show?: boolean
+ variables?: NodeOutPutVar[]
+ workflowNodesMap?: Record
+ onInsert?: () => void
+ onDelete?: () => void
+}
+
+export type MenuTextMatch = {
+ leadOffset: number
+ matchingString: string
+ replaceableString: string
+}
diff --git a/frontend/packages/common/src/components/aoplatform/prompt-editor/utils.ts b/frontend/packages/common/src/components/aoplatform/prompt-editor/utils.ts
new file mode 100644
index 00000000..f1cca6dc
--- /dev/null
+++ b/frontend/packages/common/src/components/aoplatform/prompt-editor/utils.ts
@@ -0,0 +1,354 @@
+import { $isAtNodeEnd } from '@lexical/selection'
+import type {
+ ElementNode,
+ Klass,
+ LexicalEditor,
+ LexicalNode,
+ RangeSelection,
+ TextNode,
+} from 'lexical'
+import {
+ $createTextNode,
+ $getSelection,
+ $isRangeSelection,
+ $isTextNode,
+} from 'lexical'
+import type { EntityMatch } from '@lexical/text'
+import { CustomTextNode } from './plugins/custom-text/node'
+import type { MenuTextMatch } from './types'
+import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT, MAX_VAR_KEY_LENGTH } from './constants'
+
+export function getSelectedNode(
+ selection: RangeSelection,
+): TextNode | ElementNode {
+ const anchor = selection.anchor
+ const focus = selection.focus
+ const anchorNode = selection.anchor.getNode()
+ const focusNode = selection.focus.getNode()
+ if (anchorNode === focusNode)
+ return anchorNode
+
+ const isBackward = selection.isBackward()
+ if (isBackward)
+ return $isAtNodeEnd(focus) ? anchorNode : focusNode
+ else
+ return $isAtNodeEnd(anchor) ? anchorNode : focusNode
+}
+
+export function registerLexicalTextEntity(
+ editor: LexicalEditor,
+ getMatch: (text: string) => null | EntityMatch,
+ targetNode: Klass,
+ createNode: (textNode: TextNode) => T,
+) {
+ const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
+ return node instanceof targetNode
+ }
+
+ const replaceWithSimpleText = (node: TextNode): void => {
+ const textNode = $createTextNode(node.getTextContent())
+ textNode.setFormat(node.getFormat())
+ node.replace(textNode)
+ }
+
+ const getMode = (node: TextNode): number => {
+ return node.getLatest().__mode
+ }
+
+ const textNodeTransform = (node: TextNode) => {
+ if (!node.isSimpleText())
+ return
+
+ const prevSibling = node.getPreviousSibling()
+ let text = node.getTextContent()
+ let currentNode = node
+ let match
+
+ if ($isTextNode(prevSibling)) {
+ const previousText = prevSibling.getTextContent()
+ const combinedText = previousText + text
+ const prevMatch = getMatch(combinedText)
+
+ if (isTargetNode(prevSibling)) {
+ if (prevMatch === null || getMode(prevSibling) !== 0) {
+ replaceWithSimpleText(prevSibling)
+ return
+ }
+ else {
+ const diff = prevMatch.end - previousText.length
+
+ if (diff > 0) {
+ const concatText = text.slice(0, diff)
+ const newTextContent = previousText + concatText
+ prevSibling.select()
+ prevSibling.setTextContent(newTextContent)
+
+ if (diff === text.length) {
+ node.remove()
+ }
+ else {
+ const remainingText = text.slice(diff)
+ node.setTextContent(remainingText)
+ }
+
+ return
+ }
+ }
+ }
+ else if (prevMatch === null || prevMatch.start < previousText.length) {
+ return
+ }
+ }
+
+ while (true) {
+ match = getMatch(text)
+ let nextText = match === null ? '' : text.slice(match.end)
+ text = nextText
+
+ if (nextText === '') {
+ const nextSibling = currentNode.getNextSibling()
+
+ if ($isTextNode(nextSibling)) {
+ nextText = currentNode.getTextContent() + nextSibling.getTextContent()
+ const nextMatch = getMatch(nextText)
+
+ if (nextMatch === null) {
+ if (isTargetNode(nextSibling))
+ replaceWithSimpleText(nextSibling)
+ else
+ nextSibling.markDirty()
+
+ return
+ }
+ else if (nextMatch.start !== 0) {
+ return
+ }
+ }
+ }
+ else {
+ const nextMatch = getMatch(nextText)
+
+ if (nextMatch !== null && nextMatch.start === 0)
+ return
+ }
+
+ if (match === null)
+ return
+
+ if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
+ continue
+
+ let nodeToReplace
+
+ if (match.start === 0)
+ [nodeToReplace, currentNode] = currentNode.splitText(match.end)
+ else
+ [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
+
+ const replacementNode = createNode(nodeToReplace)
+ replacementNode.setFormat(nodeToReplace.getFormat())
+ nodeToReplace.replace(replacementNode)
+
+ if (currentNode == null)
+ return
+ }
+ }
+
+ const reverseNodeTransform = (node: T) => {
+ const text = node.getTextContent()
+ const match = getMatch(text)
+
+ if (match === null || match.start !== 0) {
+ replaceWithSimpleText(node)
+ return
+ }
+
+ if (text.length > match.end) {
+ // This will split out the rest of the text as simple text
+ node.splitText(match.end)
+ return
+ }
+
+ const prevSibling = node.getPreviousSibling()
+
+ if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {
+ replaceWithSimpleText(prevSibling)
+ replaceWithSimpleText(node)
+ }
+
+ const nextSibling = node.getNextSibling()
+
+ if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
+ replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block
+
+ if (isTargetNode(node))
+ replaceWithSimpleText(node)
+ }
+ }
+
+ const removePlainTextTransform = editor.registerNodeTransform(CustomTextNode, textNodeTransform)
+ const removeReverseNodeTransform = editor.registerNodeTransform(targetNode, reverseNodeTransform)
+ return [removePlainTextTransform, removeReverseNodeTransform]
+}
+
+export const decoratorTransform = (
+ node: CustomTextNode,
+ getMatch: (text: string) => null | EntityMatch,
+ createNode: (textNode: TextNode) => LexicalNode,
+) => {
+ if (!node.isSimpleText())
+ return
+
+ const prevSibling = node.getPreviousSibling()
+ let text = node.getTextContent()
+ let currentNode = node
+ let match
+
+ while (true) {
+ match = getMatch(text)
+ let nextText = match === null ? '' : text.slice(match.end)
+ text = nextText
+
+ if (nextText === '') {
+ const nextSibling = currentNode.getNextSibling()
+
+ if ($isTextNode(nextSibling)) {
+ nextText = currentNode.getTextContent() + nextSibling.getTextContent()
+ const nextMatch = getMatch(nextText)
+
+ if (nextMatch === null) {
+ nextSibling.markDirty()
+ return
+ }
+ else if (nextMatch.start !== 0) {
+ return
+ }
+ }
+ }
+ else {
+ const nextMatch = getMatch(nextText)
+
+ if (nextMatch !== null && nextMatch.start === 0)
+ return
+ }
+
+ if (match === null)
+ return
+
+ if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())
+ continue
+
+ let nodeToReplace
+
+ if (match.start === 0)
+ [nodeToReplace, currentNode] = currentNode.splitText(match.end)
+ else
+ [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
+
+ const replacementNode = createNode(nodeToReplace)
+ nodeToReplace.replace(replacementNode)
+
+ if (currentNode == null)
+ return
+ }
+}
+
+function getFullMatchOffset(
+ documentText: string,
+ entryText: string,
+ offset: number,
+): number {
+ let triggerOffset = offset
+ for (let i = triggerOffset; i <= entryText.length; i++) {
+ if (documentText.substr(-i) === entryText.substr(0, i))
+ triggerOffset = i
+ }
+ return triggerOffset
+}
+
+export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null {
+ const selection = $getSelection()
+ if (!$isRangeSelection(selection) || !selection.isCollapsed())
+ return null
+ const anchor = selection.anchor
+ if (anchor.type !== 'text')
+ return null
+ const anchorNode = anchor.getNode()
+ if (!anchorNode.isSimpleText())
+ return null
+ const selectionOffset = anchor.offset
+ const textContent = anchorNode.getTextContent().slice(0, selectionOffset)
+ const characterOffset = match.replaceableString.length
+ const queryOffset = getFullMatchOffset(
+ textContent,
+ match.matchingString,
+ characterOffset,
+ )
+ const startOffset = selectionOffset - queryOffset
+ if (startOffset < 0)
+ return null
+ let newNode
+ if (startOffset === 0)
+ [newNode] = anchorNode.splitText(selectionOffset)
+ else
+ [, newNode] = anchorNode.splitText(startOffset, selectionOffset)
+
+ return newNode
+}
+
+export function textToEditorState(text: string) {
+ const paragraph = text.split('\n')
+
+ return JSON.stringify({
+ root: {
+ children: paragraph.map((p) => {
+ return {
+ children: [{
+ detail: 0,
+ format: 0,
+ mode: 'normal',
+ style: '',
+ text: p,
+ type: 'custom-text',
+ version: 1,
+ }],
+ direction: 'ltr',
+ format: '',
+ indent: 0,
+ type: 'paragraph',
+ version: 1,
+ }
+ }),
+ direction: 'ltr',
+ format: '',
+ indent: 0,
+ type: 'root',
+ version: 1,
+ },
+ })
+}
+
+
+
+const varRegex = /\{\{(.+?)\}\}/g
+export const getVars = (value: string) => {
+ if (!value)
+ return []
+
+ const keys = value.match(varRegex)?.filter((item) => {
+ return ![CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT].includes(item)
+ }).map((item) => {
+ return item.replace('{{', '').replace('}}', '')
+ }).filter(key => key.length <= MAX_VAR_KEY_LENGTH) || []
+ const keyObj: Record = {}
+ // remove duplicate keys
+ const res: string[] = []
+ keys.forEach((key) => {
+ if (keyObj[key])
+ return
+
+ keyObj[key] = true
+ res.push(key)
+ })
+ return res
+}
diff --git a/frontend/packages/common/src/const/approval/const.tsx b/frontend/packages/common/src/const/approval/const.tsx
index 46b82764..bf0753a7 100644
--- a/frontend/packages/common/src/const/approval/const.tsx
+++ b/frontend/packages/common/src/const/approval/const.tsx
@@ -219,6 +219,11 @@ export const ApprovalRouteColumns = [
ellipsis:true,
render:(value)=>value?.join(', ')
},
+ {
+ title:('名称'),
+ dataIndex:'name',
+ ellipsis:true,
+ },
{
title:('路径'),
dataIndex:'path',
diff --git a/frontend/packages/common/src/const/approval/type.tsx b/frontend/packages/common/src/const/approval/type.tsx
index 4af11d83..35d86bf8 100644
--- a/frontend/packages/common/src/const/approval/type.tsx
+++ b/frontend/packages/common/src/const/approval/type.tsx
@@ -106,11 +106,11 @@ export type PublishTableListItem = {
export type PublishApprovalModalProps = {
type:'approval'|'view'|'add'|'publish'|'online'
data:PublishApprovalInfoType | PublishApprovalInfoType &{id?:string} | PublishVersionTableListItem
- insideSystem?:boolean
+ insidePage?:boolean
serviceId:string
teamId:string
clusterPublishStatus?:SystemInsidePublishOnlineItems[]
-
+ serviceType?:'rest'|'ai'
}
export type PublishApprovalModalHandle = {
diff --git a/frontend/packages/common/src/const/const.tsx b/frontend/packages/common/src/const/const.tsx
index 2f85209c..3476e58f 100644
--- a/frontend/packages/common/src/const/const.tsx
+++ b/frontend/packages/common/src/const/const.tsx
@@ -26,7 +26,7 @@ export const routerKeyMap = new Map([
['operationCenter',['member','user','role','servicecategories']],
['organization',['member','user','role']],
['serviceHubSetting',['servicecategories']],
- ['maintenanceCenter',['datasourcing','cluster','cert','logsettings','resourcesettings','openapi']
+ ['maintenanceCenter',['aisetting','datasourcing','cluster','cert','logsettings','resourcesettings','openapi']
]])
@@ -43,7 +43,8 @@ export const routerKeyMap = new Map([
input:('请输入'),
select:('请选择'),
startWithAlphabet:('英文数字下划线任意一种,首字母必须为英文'),
- specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合')
+ specialStartWithAlphabet:('支持字母开头、英文数字中横线下划线组合'),
+ onlyAlphabet:('字符非法,仅支持英文'),
}
export const FORM_ERROR_TIPS = {
diff --git a/frontend/packages/common/src/const/permissions.ts b/frontend/packages/common/src/const/permissions.ts
index d7739302..954be69c 100644
--- a/frontend/packages/common/src/const/permissions.ts
+++ b/frontend/packages/common/src/const/permissions.ts
@@ -174,6 +174,16 @@ export const PERMISSION_DEFINITION = [
"anyOf": [{ "backend": ["system.devops.cluster.manager"] }]
}
},
+ "system.devops.ai_provider.view": {
+ "granted": {
+ "anyOf": [{ "backend": ["system.devops.ai_provider.view"] }]
+ }
+ },
+ "system.devops.ai_provider.edit": {
+ "granted": {
+ "anyOf": [{ "backend": ["system.devops.ai_provider.manager"] }]
+ }
+ },
"system.devops.ssl_certificate.view": {
"granted": {
"anyOf": [{ "backend": ["system.devops.ssl_certificate.view"] }]
diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx
index 5648d241..6cf49469 100644
--- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx
+++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx
@@ -4,6 +4,7 @@ import { App } from "antd";
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const";
import { checkAccess } from "@common/utils/permission";
import { PERMISSION_DEFINITION } from "@common/const/permissions";
+import { $t } from "@common/locales";
interface GlobalState {
isAuthenticated: boolean;
@@ -46,7 +47,8 @@ export const GlobalContext = createContext<{
checkPermission:(access:keyof typeof PERMISSION_DEFINITION[0] | Array)=>boolean
teamDataFlushed:boolean
accessInit:boolean
-
+ aiConfigFlushed:boolean
+ setAiConfigFlushed:(flush:boolean)=>void
} | undefined>(undefined);
const globalReducer = (state: GlobalState, action: GlobalAction): GlobalState => {
@@ -107,13 +109,14 @@ export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => {
version: '1.0.0',
updateDate: '2024-07-01',
powered:'Powered by https://apipark.com',
- mainPage:'/guide',
+ mainPage:'/guide/page',
language:'en'
});
const [accessData,setAccessData] = useState