From 0bc1f45634b1af42b63c1e4478dd4f56d00300c2 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 6 Jun 2025 17:54:59 +0800 Subject: [PATCH] Feat: Enables the message operator form to reference the data defined by the begin operator #3221 (#8108) ### What problem does this PR solve? Feat: Enables the message operator form to reference the data defined by the begin operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- .../llm-setting-items/use-watch-change.ts | 4 +- web/src/pages/agent/canvas/edge/index.tsx | 4 +- web/src/pages/agent/canvas/node/popover.tsx | 4 +- web/src/pages/agent/chat/box.tsx | 4 +- web/src/pages/agent/chat/hooks.ts | 6 +- web/src/pages/agent/constant.tsx | 15 + web/src/pages/agent/context.ts | 2 +- web/src/pages/agent/form-sheet/next.tsx | 6 +- .../agent/form/agent-form/dynamic-prompt.tsx | 2 +- web/src/pages/agent/form/agent-form/index.tsx | 11 +- .../pages/agent/form/begin-form/use-values.ts | 11 +- web/src/pages/agent/form/begin-form/utils.ts | 14 + .../pages/agent/form/components/output.tsx | 26 ++ .../form/components/prompt-editor/constant.ts | 1 + .../form/components/prompt-editor/index.css | 76 +++++ .../form/components/prompt-editor/index.tsx | 164 ++++++++++ .../form/components/prompt-editor/theme.ts | 43 +++ .../prompt-editor/variable-node.tsx | 70 +++++ .../variable-on-change-plugin.tsx | 35 +++ .../prompt-editor/variable-picker-plugin.tsx | 283 ++++++++++++++++++ .../pages/agent/form/generate-form/index.tsx | 2 +- .../pages/agent/form/message-form/index.tsx | 2 +- .../pages/agent/form/template-form/index.tsx | 2 +- web/src/pages/agent/hooks/use-build-dsl.ts | 4 +- web/src/pages/agent/hooks/use-export-json.ts | 4 +- web/src/pages/agent/hooks/use-fetch-data.ts | 4 +- .../pages/agent/hooks/use-get-begin-query.tsx | 57 ++-- web/src/pages/agent/hooks/use-save-graph.ts | 2 +- web/tailwind.config.js | 1 + web/tailwind.css | 3 + 30 files changed, 800 insertions(+), 62 deletions(-) create mode 100644 web/src/pages/agent/form/begin-form/utils.ts create mode 100644 web/src/pages/agent/form/components/output.tsx create mode 100644 web/src/pages/agent/form/components/prompt-editor/constant.ts create mode 100644 web/src/pages/agent/form/components/prompt-editor/index.css create mode 100644 web/src/pages/agent/form/components/prompt-editor/index.tsx create mode 100644 web/src/pages/agent/form/components/prompt-editor/theme.ts create mode 100644 web/src/pages/agent/form/components/prompt-editor/variable-node.tsx create mode 100644 web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx create mode 100644 web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx diff --git a/web/src/components/llm-setting-items/use-watch-change.ts b/web/src/components/llm-setting-items/use-watch-change.ts index a7ae89d90..bf3fa595c 100644 --- a/web/src/components/llm-setting-items/use-watch-change.ts +++ b/web/src/components/llm-setting-items/use-watch-change.ts @@ -1,12 +1,12 @@ import { settledModelVariableMap } from '@/constants/knowledge'; -import { FlowFormContext } from '@/pages/agent/context'; +import { AgentFormContext } from '@/pages/agent/context'; import useGraphStore from '@/pages/agent/store'; import { useCallback, useContext } from 'react'; import { useFormContext } from 'react-hook-form'; export function useHandleFreedomChange() { const form = useFormContext(); - const node = useContext(FlowFormContext); + const node = useContext(AgentFormContext); const updateNodeForm = useGraphStore((state) => state.updateNodeForm); const handleChange = useCallback( diff --git a/web/src/pages/agent/canvas/edge/index.tsx b/web/src/pages/agent/canvas/edge/index.tsx index 52f939b8d..d38b85ac0 100644 --- a/web/src/pages/agent/canvas/edge/index.tsx +++ b/web/src/pages/agent/canvas/edge/index.tsx @@ -7,7 +7,7 @@ import { import useGraphStore from '../../store'; import { useTheme } from '@/components/theme-provider'; -import { useFetchFlow } from '@/hooks/flow-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { useMemo } from 'react'; import styles from './index.less'; @@ -44,7 +44,7 @@ export function ButtonEdge({ }; // highlight the nodes that the workflow passes through - const { data: flowDetail } = useFetchFlow(); + const { data: flowDetail } = useFetchAgent(); const graphPath = useMemo(() => { // TODO: this will be called multiple times diff --git a/web/src/pages/agent/canvas/node/popover.tsx b/web/src/pages/agent/canvas/node/popover.tsx index 342ce40eb..d44538656 100644 --- a/web/src/pages/agent/canvas/node/popover.tsx +++ b/web/src/pages/agent/canvas/node/popover.tsx @@ -1,4 +1,3 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; import get from 'lodash/get'; import React, { MouseEventHandler, useCallback, useMemo } from 'react'; import JsonView from 'react18-json-view'; @@ -20,6 +19,7 @@ import { TableRow, } from '@/components/ui/table'; import { useTranslate } from '@/hooks/common-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; interface IProps extends React.PropsWithChildren { @@ -30,7 +30,7 @@ interface IProps extends React.PropsWithChildren { export function NextNodePopover({ children, nodeId, name }: IProps) { const { t } = useTranslate('flow'); - const { data } = useFetchFlow(); + const { data } = useFetchAgent(); const { theme } = useTheme(); const component = useMemo(() => { return get(data, ['dsl', 'components', nodeId], {}); diff --git a/web/src/pages/agent/chat/box.tsx b/web/src/pages/agent/chat/box.tsx index c05ae267e..1ccece7e1 100644 --- a/web/src/pages/agent/chat/box.tsx +++ b/web/src/pages/agent/chat/box.tsx @@ -9,7 +9,7 @@ import { useSendNextMessage } from './hooks'; import MessageInput from '@/components/message-input'; import PdfDrawer from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; -import { useFetchFlow } from '@/hooks/flow-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; import { buildMessageUuidWithRole } from '@/utils/chat'; @@ -30,7 +30,7 @@ const AgentChatBox = () => { useClickDrawer(); useGetFileIcon(); const { data: userInfo } = useFetchUserInfo(); - const { data: canvasInfo } = useFetchFlow(); + const { data: canvasInfo } = useFetchAgent(); return ( <> diff --git a/web/src/pages/agent/chat/hooks.ts b/web/src/pages/agent/chat/hooks.ts index 911af3923..ba4b03d39 100644 --- a/web/src/pages/agent/chat/hooks.ts +++ b/web/src/pages/agent/chat/hooks.ts @@ -1,9 +1,9 @@ import { MessageType } from '@/constants/chat'; -import { useFetchFlow } from '@/hooks/flow-hooks'; import { useHandleMessageInputChange, useSelectDerivedMessages, } from '@/hooks/logic-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { IEventList, IMessageEvent, @@ -23,7 +23,7 @@ import { receiveMessageError } from '../utils'; const antMessage = message; export const useSelectNextMessages = () => { - const { data: flowDetail, loading } = useFetchFlow(); + const { data: flowDetail, loading } = useFetchAgent(); const reference = flowDetail.dsl.reference; const { derivedMessages, @@ -69,7 +69,7 @@ export const useSendNextMessage = () => { } = useSelectNextMessages(); const { id: agentId } = useParams(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); - const { refetch } = useFetchFlow(); + const { refetch } = useFetchAgent(); const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE( api.runCanvas, diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index ebe210f25..30a103d8c 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -696,6 +696,21 @@ export const initialAgentValues = { prompts: [], message_history_window_size: 12, tools: [], + outputs: { + structured_output: { + // topic: { + // type: 'string', + // description: + // 'default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.', + // enum: ['general', 'news'], + // default: 'general', + // }, + }, + content: { + type: 'string', + value: '', + }, + }, }; export const CategorizeAnchorPointPositions = [ diff --git a/web/src/pages/agent/context.ts b/web/src/pages/agent/context.ts index fe51d8d6d..6dc1cd476 100644 --- a/web/src/pages/agent/context.ts +++ b/web/src/pages/agent/context.ts @@ -1,6 +1,6 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { createContext } from 'react'; -export const FlowFormContext = createContext( +export const AgentFormContext = createContext( undefined, ); diff --git a/web/src/pages/agent/form-sheet/next.tsx b/web/src/pages/agent/form-sheet/next.tsx index e80bedb2f..2bab4c463 100644 --- a/web/src/pages/agent/form-sheet/next.tsx +++ b/web/src/pages/agent/form-sheet/next.tsx @@ -15,7 +15,7 @@ import { Play, X } from 'lucide-react'; import { useRef } from 'react'; import { useForm } from 'react-hook-form'; import { BeginId, Operator, operatorMap } from '../constant'; -import { FlowFormContext } from '../context'; +import { AgentFormContext } from '../context'; import { RunTooltip } from '../flow-tooltip'; import { useHandleNodeNameChange } from '../hooks'; import { useHandleFormValuesChange } from '../hooks/use-watch-form-change'; @@ -145,13 +145,13 @@ const FormSheet = ({
{visible && ( - + - + )}
diff --git a/web/src/pages/agent/form/agent-form/dynamic-prompt.tsx b/web/src/pages/agent/form/agent-form/dynamic-prompt.tsx index 874e45854..0ae9330bb 100644 --- a/web/src/pages/agent/form/agent-form/dynamic-prompt.tsx +++ b/web/src/pages/agent/form/agent-form/dynamic-prompt.tsx @@ -1,4 +1,3 @@ -import { PromptEditor } from '@/components/prompt-editor'; import { BlockButton, Button } from '@/components/ui/button'; import { FormControl, @@ -12,6 +11,7 @@ import { X } from 'lucide-react'; import { memo } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { PromptEditor } from '../components/prompt-editor'; export enum PromptRole { User = 'user', diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 9aee6c99f..b4084e67e 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -2,7 +2,6 @@ import { FormContainer } from '@/components/form-container'; import { LargeModelFormField } from '@/components/large-model-form-field'; import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; -import { PromptEditor } from '@/components/prompt-editor'; import { Form, FormControl, @@ -12,6 +11,7 @@ import { } from '@/components/ui/form'; import { useFetchModelId } from '@/hooks/logic-hooks'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; @@ -19,6 +19,8 @@ import { initialAgentValues } from '../../constant'; import { useFormValues } from '../../hooks/use-form-values'; import { useWatchFormChange } from '../../hooks/use-watch-form-change'; import { INextOperatorForm } from '../../interface'; +import { Output } from '../components/output'; +import { PromptEditor } from '../components/prompt-editor'; import DynamicPrompt from './dynamic-prompt'; const FormSchema = z.object({ @@ -50,6 +52,12 @@ const AgentForm = ({ node }: INextOperatorForm) => { node, ); + const outputList = useMemo(() => { + return [ + { title: 'content', type: initialAgentValues.outputs.content.type }, + ]; + }, []); + const form = useForm({ defaultValues: defaultValues, resolver: zodResolver(FormSchema), @@ -88,6 +96,7 @@ const AgentForm = ({ node }: INextOperatorForm) => { + ); diff --git a/web/src/pages/agent/form/begin-form/use-values.ts b/web/src/pages/agent/form/begin-form/use-values.ts index df5acd834..10326bae8 100644 --- a/web/src/pages/agent/form/begin-form/use-values.ts +++ b/web/src/pages/agent/form/begin-form/use-values.ts @@ -3,7 +3,7 @@ import { isEmpty } from 'lodash'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { AgentDialogueMode } from '../../constant'; -import { BeginQuery } from '../../interface'; +import { buildBeginInputListFromObject } from './utils'; export function useValues(node?: RAGFlowNodeType) { const { t } = useTranslation(); @@ -25,14 +25,7 @@ export function useValues(node?: RAGFlowNodeType) { return defaultValues; } - const inputs = Object.entries(formData?.inputs || {}).reduce( - (pre, [key, value]) => { - pre.push({ ...(value || {}), key }); - - return pre; - }, - [], - ); + const inputs = buildBeginInputListFromObject(formData?.inputs); return { ...(formData || {}), inputs }; }, [defaultValues, node?.data?.form]); diff --git a/web/src/pages/agent/form/begin-form/utils.ts b/web/src/pages/agent/form/begin-form/utils.ts new file mode 100644 index 000000000..36038c4f6 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/utils.ts @@ -0,0 +1,14 @@ +import { BeginQuery } from '../../interface'; + +export function buildBeginInputListFromObject( + inputs: Record>, +) { + return Object.entries(inputs || {}).reduce( + (pre, [key, value]) => { + pre.push({ ...(value || {}), key }); + + return pre; + }, + [], + ); +} diff --git a/web/src/pages/agent/form/components/output.tsx b/web/src/pages/agent/form/components/output.tsx new file mode 100644 index 000000000..89ccfa8ca --- /dev/null +++ b/web/src/pages/agent/form/components/output.tsx @@ -0,0 +1,26 @@ +type OutputType = { + title: string; + type: string; +}; + +type OutputProps = { + list: Array; +}; + +export function Output({ list }: OutputProps) { + return ( +
+
Output
+
    + {list.map((x, idx) => ( +
  • + {x.title}: {x.type} +
  • + ))} +
+
+ ); +} diff --git a/web/src/pages/agent/form/components/prompt-editor/constant.ts b/web/src/pages/agent/form/components/prompt-editor/constant.ts new file mode 100644 index 000000000..b6cf30ed9 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/constant.ts @@ -0,0 +1 @@ +export const ProgrammaticTag = 'programmatic'; diff --git a/web/src/pages/agent/form/components/prompt-editor/index.css b/web/src/pages/agent/form/components/prompt-editor/index.css new file mode 100644 index 000000000..8f3050647 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/index.css @@ -0,0 +1,76 @@ +.typeahead-popover { + background: #fff; + box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3); + border-radius: 8px; + position: fixed; + z-index: 1000; +} + +.typeahead-popover ul { + list-style: none; + margin: 0; + max-height: 200px; + overflow-y: scroll; +} + +.typeahead-popover ul::-webkit-scrollbar { + display: none; +} + +.typeahead-popover ul { + -ms-overflow-style: none; + scrollbar-width: none; +} + +.typeahead-popover ul li { + margin: 0; + min-width: 180px; + font-size: 14px; + outline: none; + cursor: pointer; + border-radius: 8px; +} + +.typeahead-popover ul li.selected { + background: #eee; +} + +.typeahead-popover li { + margin: 0 8px 0 8px; + color: #050505; + cursor: pointer; + line-height: 16px; + font-size: 15px; + display: flex; + align-content: center; + flex-direction: row; + flex-shrink: 0; + background-color: #fff; + border: 0; +} + +.typeahead-popover li.active { + display: flex; + width: 20px; + height: 20px; + background-size: contain; +} + +.typeahead-popover li .text { + display: flex; + line-height: 20px; + flex-grow: 1; + min-width: 150px; +} + +.typeahead-popover li .icon { + display: flex; + width: 20px; + height: 20px; + user-select: none; + margin-right: 8px; + line-height: 16px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} diff --git a/web/src/pages/agent/form/components/prompt-editor/index.tsx b/web/src/pages/agent/form/components/prompt-editor/index.tsx new file mode 100644 index 000000000..ffda1e8c3 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/index.tsx @@ -0,0 +1,164 @@ +import { CodeHighlightNode, CodeNode } from '@lexical/code'; +import { + InitialConfigType, + LexicalComposer, +} from '@lexical/react/LexicalComposer'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { HeadingNode, QuoteNode } from '@lexical/rich-text'; +import { + $getRoot, + $getSelection, + $nodesOfType, + EditorState, + Klass, + LexicalNode, +} from 'lexical'; + +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { Variable } from 'lucide-react'; +import { ReactNode, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import theme from './theme'; +import { VariableNode } from './variable-node'; +import { VariableOnChangePlugin } from './variable-on-change-plugin'; +import VariablePickerMenuPlugin from './variable-picker-plugin'; + +// Catch any errors that occur during Lexical updates and log them +// or throw them as needed. If you don't throw them, Lexical will +// try to recover gracefully without losing user data. +function onError(error: Error) { + console.error(error); +} + +const Nodes: Array> = [ + HeadingNode, + QuoteNode, + CodeHighlightNode, + CodeNode, + VariableNode, +]; + +type PromptContentProps = { showToolbar?: boolean }; + +type IProps = { + value?: string; + onChange?: (value?: string) => void; + placeholder?: ReactNode; +} & PromptContentProps; + +function PromptContent({ showToolbar = true }: PromptContentProps) { + const [editor] = useLexicalComposerContext(); + const [isBlur, setIsBlur] = useState(false); + const { t } = useTranslation(); + + const insertTextAtCursor = useCallback(() => { + editor.update(() => { + const selection = $getSelection(); + + if (selection !== null) { + selection.insertText(' /'); + } + }); + }, [editor]); + + const handleVariableIconClick = useCallback(() => { + insertTextAtCursor(); + }, [insertTextAtCursor]); + + const handleBlur = useCallback(() => { + setIsBlur(true); + }, []); + + const handleFocus = useCallback(() => { + setIsBlur(false); + }, []); + + return ( +
+ {showToolbar && ( +
+ + + + + + + +

{t('flow.insertVariableTip')}

+
+
+
+ )} + +
+ ); +} + +export function PromptEditor({ + value, + onChange, + placeholder, + showToolbar, +}: IProps) { + const { t } = useTranslation(); + const initialConfig: InitialConfigType = { + namespace: 'PromptEditor', + theme, + onError, + nodes: Nodes, + }; + + const onValueChange = useCallback( + (editorState: EditorState) => { + editorState?.read(() => { + const listNodes = $nodesOfType(VariableNode); // to be removed + // const allNodes = $dfs(); + console.log('🚀 ~ onChange ~ allNodes:', listNodes); + + const text = $getRoot().getTextContent(); + + onChange?.(text); + }); + }, + [onChange], + ); + + return ( +
+ + + } + placeholder={ +
+ {placeholder || t('common.pleaseInput')} +
+ } + ErrorBoundary={LexicalErrorBoundary} + /> + + +
+
+ ); +} diff --git a/web/src/pages/agent/form/components/prompt-editor/theme.ts b/web/src/pages/agent/form/components/prompt-editor/theme.ts new file mode 100644 index 000000000..1cc2bc155 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/theme.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export default { + code: 'editor-code', + heading: { + h1: 'editor-heading-h1', + h2: 'editor-heading-h2', + h3: 'editor-heading-h3', + h4: 'editor-heading-h4', + h5: 'editor-heading-h5', + }, + image: 'editor-image', + link: 'editor-link', + list: { + listitem: 'editor-listitem', + nested: { + listitem: 'editor-nested-listitem', + }, + ol: 'editor-list-ol', + ul: 'editor-list-ul', + }, + ltr: 'ltr', + paragraph: 'editor-paragraph', + placeholder: 'editor-placeholder', + quote: 'editor-quote', + rtl: 'rtl', + text: { + bold: 'editor-text-bold', + code: 'editor-text-code', + hashtag: 'editor-text-hashtag', + italic: 'editor-text-italic', + overflowed: 'editor-text-overflowed', + strikethrough: 'editor-text-strikethrough', + underline: 'editor-text-underline', + underlineStrikethrough: 'editor-text-underlineStrikethrough', + }, +}; diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-node.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-node.tsx new file mode 100644 index 000000000..e2a8cc29f --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/variable-node.tsx @@ -0,0 +1,70 @@ +import i18n from '@/locales/config'; +import { BeginId } from '@/pages/flow/constant'; +import { DecoratorNode, LexicalNode, NodeKey } from 'lexical'; +import { ReactNode } from 'react'; +const prefix = BeginId + '@'; + +export class VariableNode extends DecoratorNode { + __value: string; + __label: string; + + static getType(): string { + return 'variable'; + } + + static clone(node: VariableNode): VariableNode { + return new VariableNode(node.__value, node.__label, node.__key); + } + + constructor(value: string, label: string, key?: NodeKey) { + super(key); + this.__value = value; + this.__label = label; + } + + createDOM(): HTMLElement { + const dom = document.createElement('span'); + dom.className = 'mr-1'; + + return dom; + } + + updateDOM(): false { + return false; + } + + decorate(): ReactNode { + let content: ReactNode = ( + {this.__label} + ); + if (this.__value.startsWith(prefix)) { + content = ( +
+ {i18n.t(`flow.begin`)} / {content} +
+ ); + } + return ( +
+ {content} +
+ ); + } + + getTextContent(): string { + return `{${this.__value}}`; + } +} + +export function $createVariableNode( + value: string, + label: string, +): VariableNode { + return new VariableNode(value, label); +} + +export function $isVariableNode( + node: LexicalNode | null | undefined, +): node is VariableNode { + return node instanceof VariableNode; +} diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx new file mode 100644 index 000000000..86fa66db4 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx @@ -0,0 +1,35 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { EditorState, LexicalEditor } from 'lexical'; +import { useEffect } from 'react'; +import { ProgrammaticTag } from './constant'; + +interface IProps { + onChange: ( + editorState: EditorState, + editor?: LexicalEditor, + tags?: Set, + ) => void; +} + +export function VariableOnChangePlugin({ onChange }: IProps) { + // Access the editor through the LexicalComposerContext + const [editor] = useLexicalComposerContext(); + // Wrap our listener in useEffect to handle the teardown and avoid stale references. + useEffect(() => { + // most listeners return a teardown function that can be called to clean them up. + return editor.registerUpdateListener( + ({ editorState, tags, dirtyElements }) => { + // Check if there is a "programmatic" tag + const isProgrammaticUpdate = tags.has(ProgrammaticTag); + + // The onchange event is only triggered when the data is manually updated + // Otherwise, the content will be displayed incorrectly. + if (dirtyElements.size > 0 && !isProgrammaticUpdate) { + onChange(editorState); + } + }, + ); + }, [editor, onChange]); + + return null; +} diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx new file mode 100644 index 000000000..4592e64a0 --- /dev/null +++ b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx @@ -0,0 +1,283 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { + LexicalTypeaheadMenuPlugin, + MenuOption, + useBasicTypeaheadTriggerMatch, +} from '@lexical/react/LexicalTypeaheadMenuPlugin'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + $getSelection, + $isRangeSelection, + TextNode, +} from 'lexical'; +import React, { + ReactElement, + useCallback, + useContext, + useEffect, + useRef, +} from 'react'; +import * as ReactDOM from 'react-dom'; + +import { $createVariableNode } from './variable-node'; + +import { AgentFormContext } from '@/pages/agent/context'; +import { useBuildComponentIdSelectOptions } from '@/pages/agent/hooks/use-get-begin-query'; +import { ProgrammaticTag } from './constant'; +import './index.css'; +class VariableInnerOption extends MenuOption { + label: string; + value: string; + + constructor(label: string, value: string) { + super(value); + this.label = label; + this.value = value; + } +} + +class VariableOption extends MenuOption { + label: ReactElement | string; + title: string; + options: VariableInnerOption[]; + + constructor( + label: ReactElement | string, + title: string, + options: VariableInnerOption[], + ) { + super(title); + this.label = label; + this.title = title; + this.options = options; + } +} + +function VariablePickerMenuItem({ + index, + option, + selectOptionAndCleanUp, +}: { + index: number; + option: VariableOption; + selectOptionAndCleanUp: ( + option: VariableOption | VariableInnerOption, + ) => void; +}) { + console.info('xxxx'); + return ( +
  • +
    + {option.title} +
      + {option.options.map((x) => ( +
    • selectOptionAndCleanUp(x)} + className="hover:bg-slate-300 p-1" + > + {x.label} +
    • + ))} +
    +
    +
  • + ); +} + +export default function VariablePickerMenuPlugin({ + value, +}: { + value?: string; +}): JSX.Element { + const [editor] = useLexicalComposerContext(); + const isFirstRender = useRef(true); + + const node = useContext(AgentFormContext); + + const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { + minLength: 0, + }); + + const [queryString, setQueryString] = React.useState(''); + + const buildGroupedOptions = useBuildComponentIdSelectOptions( + node?.id, + node?.parentId, + ); + + const buildNextOptions = useCallback(() => { + const options = buildGroupedOptions(); + let filteredOptions = options; + + if (queryString) { + const lowerQuery = queryString.toLowerCase(); + filteredOptions = options + .map((x) => ({ + ...x, + options: x.options.filter( + (y) => + y.label.toLowerCase().includes(lowerQuery) || + y.value.toLowerCase().includes(lowerQuery), + ), + })) + .filter((x) => x.options.length > 0); + } + + const nextOptions: VariableOption[] = filteredOptions.map( + (x) => + new VariableOption( + x.label, + x.title, + x.options.map((y) => new VariableInnerOption(y.label, y.value)), + ), + ); + + return nextOptions; + }, [buildGroupedOptions, queryString]); + + const findLabelByValue = useCallback( + (value: string) => { + const options = buildGroupedOptions(); + const children = options.reduce>( + (pre, cur) => { + return pre.concat(cur.options); + }, + [], + ); + + return children.find((x) => x.value === value)?.label; + }, + [buildGroupedOptions], + ); + + const onSelectOption = useCallback( + ( + selectedOption: VariableOption | VariableInnerOption, + nodeToRemove: TextNode | null, + closeMenu: () => void, + ) => { + editor.update(() => { + const selection = $getSelection(); + + if (!$isRangeSelection(selection) || selectedOption === null) { + return; + } + + if (nodeToRemove) { + nodeToRemove.remove(); + } + + selection.insertNodes([ + $createVariableNode( + (selectedOption as VariableInnerOption).value, + selectedOption.label as string, + ), + ]); + + closeMenu(); + }); + }, + [editor], + ); + + const parseTextToVariableNodes = useCallback( + (text: string) => { + const paragraph = $createParagraphNode(); + + // Regular expression to match content within {} + const regex = /{([^}]*)}/g; + let match; + let lastIndex = 0; + + while ((match = regex.exec(text)) !== null) { + const { 1: content, index, 0: template } = match; + + // Add the previous text part (if any) + if (index > lastIndex) { + const textNode = $createTextNode(text.slice(lastIndex, index)); + + paragraph.append(textNode); + } + + // Add variable node or text node + const label = findLabelByValue(content); + if (label) { + paragraph.append($createVariableNode(content, label)); + } else { + paragraph.append($createTextNode(template)); + } + + // Update index + lastIndex = regex.lastIndex; + } + + // Add the last part of text (if any) + if (lastIndex < text.length) { + const textNode = $createTextNode(text.slice(lastIndex)); + paragraph.append(textNode); + } + + $getRoot().clear().append(paragraph); + }, + [findLabelByValue], + ); + + useEffect(() => { + if (editor && value && isFirstRender.current) { + isFirstRender.current = false; + editor.update( + () => { + parseTextToVariableNodes(value); + }, + { tag: ProgrammaticTag }, + ); + } + }, [parseTextToVariableNodes, editor, value]); + + return ( + + onQueryChange={setQueryString} + onSelectOption={onSelectOption} + triggerFn={checkForTriggerMatch} + options={buildNextOptions()} + menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { + const nextOptions = buildNextOptions(); + console.log('🚀 ~ nextOptions:', nextOptions); + return anchorElementRef.current && nextOptions.length + ? ReactDOM.createPortal( +
    +
      + {nextOptions.map((option, i: number) => ( + + ))} +
    +
    , + anchorElementRef.current, + ) + : null; + }} + /> + ); +} diff --git a/web/src/pages/agent/form/generate-form/index.tsx b/web/src/pages/agent/form/generate-form/index.tsx index d463c7114..8ca644b87 100644 --- a/web/src/pages/agent/form/generate-form/index.tsx +++ b/web/src/pages/agent/form/generate-form/index.tsx @@ -1,6 +1,5 @@ import { NextLLMSelect } from '@/components/llm-select/next'; import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; -import { PromptEditor } from '@/components/prompt-editor'; import { Form, FormControl, @@ -12,6 +11,7 @@ import { import { Switch } from '@/components/ui/switch'; import { useTranslation } from 'react-i18next'; import { INextOperatorForm } from '../../interface'; +import { PromptEditor } from '../components/prompt-editor'; const GenerateForm = ({ form }: INextOperatorForm) => { const { t } = useTranslation(); diff --git a/web/src/pages/agent/form/message-form/index.tsx b/web/src/pages/agent/form/message-form/index.tsx index cc910c4b6..b6f67b5d8 100644 --- a/web/src/pages/agent/form/message-form/index.tsx +++ b/web/src/pages/agent/form/message-form/index.tsx @@ -1,5 +1,4 @@ import { FormContainer } from '@/components/form-container'; -import { PromptEditor } from '@/components/prompt-editor'; import { BlockButton, Button } from '@/components/ui/button'; import { Form, @@ -15,6 +14,7 @@ import { useFieldArray, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { INextOperatorForm } from '../../interface'; +import { PromptEditor } from '../components/prompt-editor'; import { useValues } from './use-values'; import { useWatchFormChange } from './use-watch-change'; diff --git a/web/src/pages/agent/form/template-form/index.tsx b/web/src/pages/agent/form/template-form/index.tsx index ddf5c7883..f9968a4c9 100644 --- a/web/src/pages/agent/form/template-form/index.tsx +++ b/web/src/pages/agent/form/template-form/index.tsx @@ -1,7 +1,7 @@ -import { PromptEditor } from '@/components/prompt-editor'; import { Form } from 'antd'; import { useTranslation } from 'react-i18next'; import { IOperatorForm } from '../../interface'; +import { PromptEditor } from '../components/prompt-editor'; const TemplateForm = ({ onValuesChange, form }: IOperatorForm) => { const { t } = useTranslation(); diff --git a/web/src/pages/agent/hooks/use-build-dsl.ts b/web/src/pages/agent/hooks/use-build-dsl.ts index 17a0681ed..eb32b2317 100644 --- a/web/src/pages/agent/hooks/use-build-dsl.ts +++ b/web/src/pages/agent/hooks/use-build-dsl.ts @@ -1,11 +1,11 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { useCallback } from 'react'; import useGraphStore from '../store'; import { buildDslComponentsByGraph } from '../utils'; export const useBuildDslData = () => { - const { data } = useFetchFlow(); + const { data } = useFetchAgent(); const { nodes, edges } = useGraphStore((state) => state); const buildDslData = useCallback( diff --git a/web/src/pages/agent/hooks/use-export-json.ts b/web/src/pages/agent/hooks/use-export-json.ts index 9bffd47a9..1efe6bb50 100644 --- a/web/src/pages/agent/hooks/use-export-json.ts +++ b/web/src/pages/agent/hooks/use-export-json.ts @@ -1,7 +1,7 @@ import { useToast } from '@/components/hooks/use-toast'; import { FileMimeType, Platform } from '@/constants/common'; import { useSetModalState } from '@/hooks/common-hooks'; -import { useFetchFlow } from '@/hooks/flow-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { IGraph } from '@/interfaces/database/flow'; import { downloadJsonFile } from '@/utils/file-util'; import { message } from 'antd'; @@ -19,7 +19,7 @@ export const useHandleExportOrImportJsonFile = () => { showModal: showFileUploadModal, } = useSetModalState(); const setGraphInfo = useSetGraphInfo(); - const { data } = useFetchFlow(); + const { data } = useFetchAgent(); const { t } = useTranslation(); const { toast } = useToast(); diff --git a/web/src/pages/agent/hooks/use-fetch-data.ts b/web/src/pages/agent/hooks/use-fetch-data.ts index 245ea6abf..5a1ca40cb 100644 --- a/web/src/pages/agent/hooks/use-fetch-data.ts +++ b/web/src/pages/agent/hooks/use-fetch-data.ts @@ -1,10 +1,10 @@ -import { useFetchFlow } from '@/hooks/flow-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; import { IGraph } from '@/interfaces/database/flow'; import { useEffect } from 'react'; import { useSetGraphInfo } from './use-set-graph'; export const useFetchDataOnMount = () => { - const { loading, data, refetch } = useFetchFlow(); + const { loading, data, refetch } = useFetchAgent(); const setGraphInfo = useSetGraphInfo(); useEffect(() => { diff --git a/web/src/pages/agent/hooks/use-get-begin-query.tsx b/web/src/pages/agent/hooks/use-get-begin-query.tsx index 557bbfce0..052b51af0 100644 --- a/web/src/pages/agent/hooks/use-get-begin-query.tsx +++ b/web/src/pages/agent/hooks/use-get-begin-query.tsx @@ -3,6 +3,7 @@ import { DefaultOptionType } from 'antd/es/select'; import get from 'lodash/get'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { BeginId, Operator } from '../constant'; +import { buildBeginInputListFromObject } from '../form/begin-form/utils'; import { BeginQuery } from '../interface'; import useGraphStore from '../store'; @@ -10,7 +11,9 @@ export const useGetBeginNodeDataQuery = () => { const getNode = useGraphStore((state) => state.getNode); const getBeginNodeDataQuery = useCallback(() => { - return get(getNode(BeginId), 'data.form.query', []); + return buildBeginInputListFromObject( + get(getNode(BeginId), 'data.form.inputs', {}), + ); }, [getNode]); return getBeginNodeDataQuery; @@ -45,7 +48,6 @@ export const useBuildComponentIdSelectOptions = ( ) => { const nodes = useGraphStore((state) => state.nodes); const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const query: BeginQuery[] = getBeginNodeDataQuery(); // Limit the nodes inside iteration to only reference peer nodes with the same parentId and other external nodes other than their parent nodes const filterChildNodesToSameParentOrExternal = useCallback( @@ -74,34 +76,37 @@ export const useBuildComponentIdSelectOptions = ( .map((x) => ({ label: x.data.name, value: x.id })); }, [nodes, nodeId, filterChildNodesToSameParentOrExternal]); - const groupedOptions = [ - { - label: Component Output, - title: 'Component Output', - options: componentIdOptions, - }, - { - label: Begin Input, - title: 'Begin Input', - options: query.map((x) => ({ - label: x.name, - value: `begin@${x.key}`, - })), - }, - ]; + const buildGroupedOptions = useCallback(() => { + const query: BeginQuery[] = getBeginNodeDataQuery(); + return [ + { + label: Component Output, + title: 'Component Output', + options: componentIdOptions, + }, + { + label: Begin Input, + title: 'Begin Input', + options: query.map((x) => ({ + label: x.name, + value: `begin@${x.key}`, + })), + }, + ]; + }, [componentIdOptions, getBeginNodeDataQuery]); - return groupedOptions; + return buildGroupedOptions; }; export const useGetComponentLabelByValue = (nodeId: string) => { - const options = useBuildComponentIdSelectOptions(nodeId); - const flattenOptions = useMemo( - () => - options.reduce((pre, cur) => { - return [...pre, ...cur.options]; - }, []), - [options], - ); + const buildGroupedOptions = useBuildComponentIdSelectOptions(nodeId); + + const flattenOptions = useMemo(() => { + const options = buildGroupedOptions(); + return options.reduce((pre, cur) => { + return [...pre, ...cur.options]; + }, []); + }, [buildGroupedOptions]); const getLabel = useCallback( (val?: string) => { diff --git a/web/src/pages/agent/hooks/use-save-graph.ts b/web/src/pages/agent/hooks/use-save-graph.ts index 1023375bb..948c66f25 100644 --- a/web/src/pages/agent/hooks/use-save-graph.ts +++ b/web/src/pages/agent/hooks/use-save-graph.ts @@ -25,7 +25,7 @@ export const useSaveGraph = () => { dsl: buildDslData(currentNodes), }); }, - [setAgent, id, data.title, buildDslData], + [setAgent, data, id, buildDslData], ); return { saveGraph, loading }; diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 4f097dd19..f654ac2e2 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -51,6 +51,7 @@ module.exports = { 'background-header-bar': 'var(--background-header-bar)', 'background-card': 'var(--background-card)', 'background-checked': 'var(--background-checked)', + 'background-highlight': 'var(--background-highlight)', 'input-border': 'var(--input-border)', diff --git a/web/tailwind.css b/web/tailwind.css index 0c58eef19..45deb2d78 100644 --- a/web/tailwind.css +++ b/web/tailwind.css @@ -86,6 +86,7 @@ --background-card: rgba(22, 22, 24, 0.05); --background-checked: rgba(76, 164, 231, 1); + --background-highlight: rgba(76, 164, 231, 0.1); --input-border: rgba(22, 22, 24, 0.2); } @@ -195,6 +196,8 @@ --background-card: rgba(255, 255, 255, 0.05); --background-checked: rgba(76, 164, 231, 1); + --background-highlight: rgba(76, 164, 231, 0.1); + --input-border: rgba(255, 255, 255, 0.2); } }