### What problem does this PR solve? Feat: Add DynamicPrompt component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| VariableNode, | VariableNode, | ||||
| ]; | ]; | ||||
| type PromptContentProps = { showToolbar?: boolean }; | |||||
| type IProps = { | type IProps = { | ||||
| value?: string; | value?: string; | ||||
| onChange?: (value?: string) => void; | onChange?: (value?: string) => void; | ||||
| placeholder?: ReactNode; | placeholder?: ReactNode; | ||||
| }; | |||||
| } & PromptContentProps; | |||||
| function PromptContent() { | |||||
| function PromptContent({ showToolbar = true }: PromptContentProps) { | |||||
| const [editor] = useLexicalComposerContext(); | const [editor] = useLexicalComposerContext(); | ||||
| const [isBlur, setIsBlur] = useState(false); | const [isBlur, setIsBlur] = useState(false); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| <section | <section | ||||
| className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })} | className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })} | ||||
| > | > | ||||
| <div className="border-b px-2 py-2 justify-end flex"> | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <span className="inline-block cursor-pointer cursor p-0.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-sm"> | |||||
| <Variable size={16} onClick={handleVariableIconClick} /> | |||||
| </span> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent> | |||||
| <p>{t('flow.insertVariableTip')}</p> | |||||
| </TooltipContent> | |||||
| </Tooltip> | |||||
| </div> | |||||
| {showToolbar && ( | |||||
| <div className="border-b px-2 py-2 justify-end flex"> | |||||
| <Tooltip> | |||||
| <TooltipTrigger asChild> | |||||
| <span className="inline-block cursor-pointer cursor p-0.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-sm"> | |||||
| <Variable size={16} onClick={handleVariableIconClick} /> | |||||
| </span> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent> | |||||
| <p>{t('flow.insertVariableTip')}</p> | |||||
| </TooltipContent> | |||||
| </Tooltip> | |||||
| </div> | |||||
| )} | |||||
| <ContentEditable | <ContentEditable | ||||
| className="min-h-40 relative px-2 py-1 focus-visible:outline-none" | className="min-h-40 relative px-2 py-1 focus-visible:outline-none" | ||||
| onBlur={handleBlur} | onBlur={handleBlur} | ||||
| ); | ); | ||||
| } | } | ||||
| export function PromptEditor({ value, onChange, placeholder }: IProps) { | |||||
| export function PromptEditor({ | |||||
| value, | |||||
| onChange, | |||||
| placeholder, | |||||
| showToolbar, | |||||
| }: IProps) { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const initialConfig: InitialConfigType = { | const initialConfig: InitialConfigType = { | ||||
| namespace: 'PromptEditor', | namespace: 'PromptEditor', | ||||
| <div className="relative"> | <div className="relative"> | ||||
| <LexicalComposer initialConfig={initialConfig}> | <LexicalComposer initialConfig={initialConfig}> | ||||
| <RichTextPlugin | <RichTextPlugin | ||||
| contentEditable={<PromptContent></PromptContent>} | |||||
| contentEditable={ | |||||
| <PromptContent showToolbar={showToolbar}></PromptContent> | |||||
| } | |||||
| placeholder={ | placeholder={ | ||||
| <div | <div | ||||
| className="absolute top-10 left-2 text-text-sub-title" | className="absolute top-10 left-2 text-text-sub-title" |
| beginInputTip: | beginInputTip: | ||||
| 'By defining input parameters, this content can be accessed by other components in subsequent processes.', | 'By defining input parameters, this content can be accessed by other components in subsequent processes.', | ||||
| query: 'Query variables', | query: 'Query variables', | ||||
| agent: 'Agent', | |||||
| agentDescription: | |||||
| 'Builds agent components equipped with reasoning, tool usage, and multi-agent collaboration. ', | |||||
| }, | }, | ||||
| llmTools: { | llmTools: { | ||||
| bad_calculator: { | bad_calculator: { |
| modeTip: '模式定义了工作流的启动方式。', | modeTip: '模式定义了工作流的启动方式。', | ||||
| beginInputTip: '通过定义输入参数,此内容可以被后续流程中的其他组件访问。', | beginInputTip: '通过定义输入参数,此内容可以被后续流程中的其他组件访问。', | ||||
| query: '查询变量', | query: '查询变量', | ||||
| agent: 'Agent', | |||||
| agentDescription: '构建具备推理、工具调用和多智能体协同的智能体组件。', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: 'All rights reserved @ React', | profile: 'All rights reserved @ React', |
| import { PromptEditor } from '@/components/prompt-editor'; | |||||
| import { BlockButton, Button } from '@/components/ui/button'; | |||||
| import { | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||||
| import { X } from 'lucide-react'; | |||||
| import { memo } from 'react'; | |||||
| import { useFieldArray, useFormContext } from 'react-hook-form'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| export enum PromptRole { | |||||
| User = 'user', | |||||
| Assistant = 'assistant', | |||||
| } | |||||
| const options = [ | |||||
| { label: 'User', value: PromptRole.User }, | |||||
| { label: 'Assistant', value: PromptRole.Assistant }, | |||||
| ]; | |||||
| const DynamicPrompt = () => { | |||||
| const { t } = useTranslation(); | |||||
| const form = useFormContext(); | |||||
| const name = 'prompts'; | |||||
| const { fields, append, remove } = useFieldArray({ | |||||
| name: name, | |||||
| control: form.control, | |||||
| }); | |||||
| return ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel> | |||||
| <div className="space-y-4"> | |||||
| {fields.map((field, index) => ( | |||||
| <div key={field.id} className="flex"> | |||||
| <div className="space-y-2 flex-1"> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={`${name}.${index}.role`} | |||||
| render={({ field }) => ( | |||||
| <FormItem className="w-1/3"> | |||||
| <FormLabel /> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={options} | |||||
| ></RAGFlowSelect> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={`${name}.${index}.content`} | |||||
| render={({ field }) => ( | |||||
| <FormItem className="flex-1"> | |||||
| <FormControl> | |||||
| <section> | |||||
| <PromptEditor | |||||
| {...field} | |||||
| showToolbar={false} | |||||
| ></PromptEditor> | |||||
| </section> | |||||
| </FormControl> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| </div> | |||||
| <Button | |||||
| type="button" | |||||
| variant={'ghost'} | |||||
| onClick={() => remove(index)} | |||||
| > | |||||
| <X /> | |||||
| </Button> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| <FormMessage /> | |||||
| <BlockButton | |||||
| onClick={() => append({ content: '', role: PromptRole.User })} | |||||
| > | |||||
| Add | |||||
| </BlockButton> | |||||
| </FormItem> | |||||
| ); | |||||
| }; | |||||
| export default memo(DynamicPrompt); |
| import { FormContainer } from '@/components/form-container'; | import { FormContainer } from '@/components/form-container'; | ||||
| import { LargeModelFormField } from '@/components/large-model-form-field'; | import { LargeModelFormField } from '@/components/large-model-form-field'; | ||||
| import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | ||||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||||
| import { PromptEditor } from '@/components/prompt-editor'; | import { PromptEditor } from '@/components/prompt-editor'; | ||||
| import { Form, FormControl, FormField, FormItem } from '@/components/ui/form'; | |||||
| import { | |||||
| Form, | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| } from '@/components/ui/form'; | |||||
| import { useFetchModelId } from '@/hooks/logic-hooks'; | |||||
| import { zodResolver } from '@hookform/resolvers/zod'; | import { zodResolver } from '@hookform/resolvers/zod'; | ||||
| import { useForm } from 'react-hook-form'; | import { useForm } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||
| import { initialAgentValues } from '../../constant'; | import { initialAgentValues } from '../../constant'; | ||||
| import { useFormValues } from '../../hooks/use-form-values'; | import { useFormValues } from '../../hooks/use-form-values'; | ||||
| import { useWatchFormChange } from '../../hooks/use-watch-form-change'; | |||||
| import { INextOperatorForm } from '../../interface'; | import { INextOperatorForm } from '../../interface'; | ||||
| import DynamicPrompt from './dynamic-prompt'; | |||||
| const FormSchema = z.object({ | const FormSchema = z.object({ | ||||
| sys_prompt: z.string(), | sys_prompt: z.string(), | ||||
| ) | ) | ||||
| .optional(), | .optional(), | ||||
| message_history_window_size: z.coerce.number(), | message_history_window_size: z.coerce.number(), | ||||
| ...LlmSettingSchema, | |||||
| tools: z | tools: z | ||||
| .array( | .array( | ||||
| z.object({ | z.object({ | ||||
| }), | }), | ||||
| ) | ) | ||||
| .optional(), | .optional(), | ||||
| ...LlmSettingSchema, | |||||
| }); | }); | ||||
| const AgentForm = ({ node }: INextOperatorForm) => { | const AgentForm = ({ node }: INextOperatorForm) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const defaultValues = useFormValues(initialAgentValues, node); | |||||
| const llmId = useFetchModelId(); | |||||
| const defaultValues = useFormValues( | |||||
| { ...initialAgentValues, llm_id: llmId }, | |||||
| node, | |||||
| ); | |||||
| const form = useForm({ | const form = useForm({ | ||||
| defaultValues: defaultValues, | defaultValues: defaultValues, | ||||
| resolver: zodResolver(FormSchema), | resolver: zodResolver(FormSchema), | ||||
| }); | }); | ||||
| useWatchFormChange(node?.id, form); | |||||
| return ( | return ( | ||||
| <Form {...form}> | <Form {...form}> | ||||
| <form | <form | ||||
| name={`sys_prompt`} | name={`sys_prompt`} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem className="flex-1"> | <FormItem className="flex-1"> | ||||
| <FormLabel>Prompt</FormLabel> | |||||
| <FormControl> | <FormControl> | ||||
| <PromptEditor | <PromptEditor | ||||
| {...field} | {...field} | ||||
| placeholder={t('flow.messagePlaceholder')} | placeholder={t('flow.messagePlaceholder')} | ||||
| showToolbar={false} | |||||
| ></PromptEditor> | ></PromptEditor> | ||||
| </FormControl> | </FormControl> | ||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> | |||||
| </FormContainer> | |||||
| <FormContainer> | |||||
| <DynamicPrompt></DynamicPrompt> | |||||
| </FormContainer> | </FormContainer> | ||||
| </form> | </form> | ||||
| </Form> | </Form> |
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||
| import { AgentDialogueMode } from '../../constant'; | import { AgentDialogueMode } from '../../constant'; | ||||
| import { useWatchFormChange } from '../../hooks/use-watch-form-change'; | |||||
| import { INextOperatorForm } from '../../interface'; | import { INextOperatorForm } from '../../interface'; | ||||
| import { ParameterDialog } from './parameter-dialog'; | import { ParameterDialog } from './parameter-dialog'; | ||||
| import { QueryTable } from './query-table'; | import { QueryTable } from './query-table'; | ||||
| import { useEditQueryRecord } from './use-edit-query'; | import { useEditQueryRecord } from './use-edit-query'; | ||||
| import { useValues } from './use-values'; | import { useValues } from './use-values'; | ||||
| import { useWatchFormChange } from './use-watch-change'; | |||||
| const ModeOptions = buildSelectOptions([ | const ModeOptions = buildSelectOptions([ | ||||
| AgentDialogueMode.Conversational, | AgentDialogueMode.Conversational, |
| import { useEffect } from 'react'; | |||||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||||
| import useGraphStore from '../../store'; | |||||
| export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||||
| let values = useWatch({ control: form?.control }); | |||||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||||
| useEffect(() => { | |||||
| // Manually triggered form updates are synchronized to the canvas | |||||
| if (id && form?.formState.isDirty) { | |||||
| values = form?.getValues(); | |||||
| let nextValues: any = values; | |||||
| updateNodeForm(id, nextValues); | |||||
| } | |||||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||||
| } |
| [Operator.IterationStart]: initialIterationValues, | [Operator.IterationStart]: initialIterationValues, | ||||
| [Operator.Code]: initialCodeValues, | [Operator.Code]: initialCodeValues, | ||||
| [Operator.WaitingDialogue]: initialWaitingDialogueValues, | [Operator.WaitingDialogue]: initialWaitingDialogueValues, | ||||
| [Operator.Agent]: initialAgentValues, | |||||
| [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, | |||||
| }; | }; | ||||
| }, [llmId]); | }, [llmId]); | ||||
| return { handleValuesChange }; | return { handleValuesChange }; | ||||
| }; | }; | ||||
| export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||||
| let values = useWatch({ control: form?.control }); | |||||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||||
| useEffect(() => { | |||||
| // Manually triggered form updates are synchronized to the canvas | |||||
| if (id && form?.formState.isDirty) { | |||||
| values = form?.getValues(); | |||||
| let nextValues: any = values; | |||||
| updateNodeForm(id, nextValues); | |||||
| } | |||||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||||
| } |