### 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
| @@ -42,13 +42,15 @@ const Nodes: Array<Klass<LexicalNode>> = [ | |||
| VariableNode, | |||
| ]; | |||
| type PromptContentProps = { showToolbar?: boolean }; | |||
| type IProps = { | |||
| value?: string; | |||
| onChange?: (value?: string) => void; | |||
| placeholder?: ReactNode; | |||
| }; | |||
| } & PromptContentProps; | |||
| function PromptContent() { | |||
| function PromptContent({ showToolbar = true }: PromptContentProps) { | |||
| const [editor] = useLexicalComposerContext(); | |||
| const [isBlur, setIsBlur] = useState(false); | |||
| const { t } = useTranslation(); | |||
| @@ -79,18 +81,20 @@ function PromptContent() { | |||
| <section | |||
| 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 | |||
| className="min-h-40 relative px-2 py-1 focus-visible:outline-none" | |||
| onBlur={handleBlur} | |||
| @@ -100,7 +104,12 @@ function PromptContent() { | |||
| ); | |||
| } | |||
| export function PromptEditor({ value, onChange, placeholder }: IProps) { | |||
| export function PromptEditor({ | |||
| value, | |||
| onChange, | |||
| placeholder, | |||
| showToolbar, | |||
| }: IProps) { | |||
| const { t } = useTranslation(); | |||
| const initialConfig: InitialConfigType = { | |||
| namespace: 'PromptEditor', | |||
| @@ -128,7 +137,9 @@ export function PromptEditor({ value, onChange, placeholder }: IProps) { | |||
| <div className="relative"> | |||
| <LexicalComposer initialConfig={initialConfig}> | |||
| <RichTextPlugin | |||
| contentEditable={<PromptContent></PromptContent>} | |||
| contentEditable={ | |||
| <PromptContent showToolbar={showToolbar}></PromptContent> | |||
| } | |||
| placeholder={ | |||
| <div | |||
| className="absolute top-10 left-2 text-text-sub-title" | |||
| @@ -1279,6 +1279,9 @@ This delimiter is used to split the input text into several text pieces echo of | |||
| beginInputTip: | |||
| 'By defining input parameters, this content can be accessed by other components in subsequent processes.', | |||
| query: 'Query variables', | |||
| agent: 'Agent', | |||
| agentDescription: | |||
| 'Builds agent components equipped with reasoning, tool usage, and multi-agent collaboration. ', | |||
| }, | |||
| llmTools: { | |||
| bad_calculator: { | |||
| @@ -1232,6 +1232,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| modeTip: '模式定义了工作流的启动方式。', | |||
| beginInputTip: '通过定义输入参数,此内容可以被后续流程中的其他组件访问。', | |||
| query: '查询变量', | |||
| agent: 'Agent', | |||
| agentDescription: '构建具备推理、工具调用和多智能体协同的智能体组件。', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -0,0 +1,97 @@ | |||
| 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); | |||
| @@ -1,15 +1,25 @@ | |||
| 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, 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 { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { initialAgentValues } from '../../constant'; | |||
| import { useFormValues } from '../../hooks/use-form-values'; | |||
| import { useWatchFormChange } from '../../hooks/use-watch-form-change'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import DynamicPrompt from './dynamic-prompt'; | |||
| const FormSchema = z.object({ | |||
| sys_prompt: z.string(), | |||
| @@ -22,7 +32,6 @@ const FormSchema = z.object({ | |||
| ) | |||
| .optional(), | |||
| message_history_window_size: z.coerce.number(), | |||
| ...LlmSettingSchema, | |||
| tools: z | |||
| .array( | |||
| z.object({ | |||
| @@ -30,17 +39,24 @@ const FormSchema = z.object({ | |||
| }), | |||
| ) | |||
| .optional(), | |||
| ...LlmSettingSchema, | |||
| }); | |||
| const AgentForm = ({ node }: INextOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| const defaultValues = useFormValues(initialAgentValues, node); | |||
| const llmId = useFetchModelId(); | |||
| const defaultValues = useFormValues( | |||
| { ...initialAgentValues, llm_id: llmId }, | |||
| node, | |||
| ); | |||
| const form = useForm({ | |||
| defaultValues: defaultValues, | |||
| resolver: zodResolver(FormSchema), | |||
| }); | |||
| useWatchFormChange(node?.id, form); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| @@ -56,15 +72,21 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| name={`sys_prompt`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1"> | |||
| <FormLabel>Prompt</FormLabel> | |||
| <FormControl> | |||
| <PromptEditor | |||
| {...field} | |||
| placeholder={t('flow.messagePlaceholder')} | |||
| showToolbar={false} | |||
| ></PromptEditor> | |||
| </FormControl> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> | |||
| </FormContainer> | |||
| <FormContainer> | |||
| <DynamicPrompt></DynamicPrompt> | |||
| </FormContainer> | |||
| </form> | |||
| </Form> | |||
| @@ -20,12 +20,12 @@ import { useForm, useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { AgentDialogueMode } from '../../constant'; | |||
| import { useWatchFormChange } from '../../hooks/use-watch-form-change'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { ParameterDialog } from './parameter-dialog'; | |||
| import { QueryTable } from './query-table'; | |||
| import { useEditQueryRecord } from './use-edit-query'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| const ModeOptions = buildSelectOptions([ | |||
| AgentDialogueMode.Conversational, | |||
| @@ -1,18 +0,0 @@ | |||
| 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]); | |||
| } | |||
| @@ -146,7 +146,7 @@ export const useInitializeOperatorParams = () => { | |||
| [Operator.IterationStart]: initialIterationValues, | |||
| [Operator.Code]: initialCodeValues, | |||
| [Operator.WaitingDialogue]: initialWaitingDialogueValues, | |||
| [Operator.Agent]: initialAgentValues, | |||
| [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, | |||
| }; | |||
| }, [llmId]); | |||
| @@ -149,3 +149,18 @@ export const useHandleFormValuesChange = ( | |||
| 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]); | |||
| } | |||