### What problem does this PR solve? Feat: Add UserFillUpForm component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -67,7 +67,9 @@ function AccordionOperators() { | |||
| <AccordionItem value="item-2"> | |||
| <AccordionTrigger className="text-xl">Dialogue </AccordionTrigger> | |||
| <AccordionContent className="flex flex-col gap-4 text-balance"> | |||
| <OperatorItemList operators={[Operator.Message]}></OperatorItemList> | |||
| <OperatorItemList | |||
| operators={[Operator.Message, Operator.UserFillUp]} | |||
| ></OperatorItemList> | |||
| </AccordionContent> | |||
| </AccordionItem> | |||
| <AccordionItem value="item-3"> | |||
| @@ -86,6 +86,7 @@ export enum Operator { | |||
| Agent = 'Agent', | |||
| Tool = 'Tool', | |||
| TavilySearch = 'TavilySearch', | |||
| UserFillUp = 'UserFillUp', | |||
| } | |||
| export const SwitchLogicOperatorOptions = ['and', 'or']; | |||
| @@ -696,6 +697,12 @@ export const initialAgentValues = { | |||
| }, | |||
| }; | |||
| export const initialUserFillUpValues = { | |||
| enable_tips: true, | |||
| tips: '', | |||
| inputs: [], | |||
| }; | |||
| export enum TavilySearchDepth { | |||
| Basic = 'basic', | |||
| Advanced = 'advanced', | |||
| @@ -860,6 +867,7 @@ export const NodeMap = { | |||
| [Operator.Agent]: 'agentNode', | |||
| [Operator.Tool]: 'toolNode', | |||
| [Operator.TavilySearch]: 'ragNode', | |||
| [Operator.UserFillUp]: 'ragNode', | |||
| }; | |||
| export enum BeginQueryType { | |||
| @@ -38,6 +38,7 @@ import TavilyForm from '../form/tavily-form'; | |||
| import TemplateForm from '../form/template-form'; | |||
| import ToolForm from '../form/tool-form'; | |||
| import TuShareForm from '../form/tushare-form'; | |||
| import UserFillUpForm from '../form/user-fill-up-form'; | |||
| import WenCaiForm from '../form/wencai-form'; | |||
| import WikipediaForm from '../form/wikipedia-form'; | |||
| import YahooFinanceForm from '../form/yahoo-finance-form'; | |||
| @@ -382,6 +383,11 @@ export function useFormConfigMap() { | |||
| defaultValues: {}, | |||
| schema: z.object({}), | |||
| }, | |||
| [Operator.UserFillUp]: { | |||
| component: UserFillUpForm, | |||
| defaultValues: {}, | |||
| schema: z.object({}), | |||
| }, | |||
| }; | |||
| return FormConfigMap; | |||
| @@ -0,0 +1,155 @@ | |||
| import { Collapse } from '@/components/collapse'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| import { FormTooltip } from '@/components/ui/tooltip'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { ParameterDialog } from '../begin-form/parameter-dialog'; | |||
| import { QueryTable } from '../begin-form/query-table'; | |||
| import { useEditQueryRecord } from '../begin-form/use-edit-query'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| const UserFillUpForm = ({ node }: INextOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| const values = useValues(node); | |||
| const FormSchema = z.object({ | |||
| enable_tips: z.boolean().optional(), | |||
| tips: z.string().trim().optional(), | |||
| inputs: z | |||
| .array( | |||
| z.object({ | |||
| key: z.string(), | |||
| type: z.string(), | |||
| value: z.string(), | |||
| optional: z.boolean(), | |||
| name: z.string(), | |||
| options: z.array(z.union([z.number(), z.string(), z.boolean()])), | |||
| }), | |||
| ) | |||
| .optional(), | |||
| }); | |||
| const form = useForm({ | |||
| defaultValues: values, | |||
| resolver: zodResolver(FormSchema), | |||
| }); | |||
| useWatchFormChange(node?.id, form); | |||
| const inputs = useWatch({ control: form.control, name: 'inputs' }); | |||
| const { | |||
| ok, | |||
| currentRecord, | |||
| visible, | |||
| hideModal, | |||
| showModal, | |||
| otherThanCurrentQuery, | |||
| handleDeleteRecord, | |||
| } = useEditQueryRecord({ | |||
| form, | |||
| node, | |||
| }); | |||
| return ( | |||
| <section className="px-5 space-y-5"> | |||
| <Form {...form}> | |||
| <FormField | |||
| control={form.control} | |||
| name={'enable_tips'} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('flow.openingSwitchTip')}> | |||
| Guiding Question | |||
| </FormLabel> | |||
| <FormControl> | |||
| <Switch | |||
| checked={field.value} | |||
| onCheckedChange={field.onChange} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name={'tips'} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('chat.setAnOpenerTip')}>Message</FormLabel> | |||
| <FormControl> | |||
| <Textarea | |||
| rows={5} | |||
| {...field} | |||
| placeholder={t('common.pleaseInput')} | |||
| ></Textarea> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| {/* Create a hidden field to make Form instance record this */} | |||
| <FormField | |||
| control={form.control} | |||
| name={'inputs'} | |||
| render={() => <div></div>} | |||
| /> | |||
| <Collapse | |||
| title={ | |||
| <div> | |||
| {t('flow.input')} | |||
| <FormTooltip tooltip={t('flow.beginInputTip')}></FormTooltip> | |||
| </div> | |||
| } | |||
| rightContent={ | |||
| <Button | |||
| variant={'ghost'} | |||
| onClick={(e) => { | |||
| e.preventDefault(); | |||
| showModal(); | |||
| }} | |||
| > | |||
| <Plus /> | |||
| </Button> | |||
| } | |||
| > | |||
| <QueryTable | |||
| data={inputs} | |||
| showModal={showModal} | |||
| deleteRecord={handleDeleteRecord} | |||
| ></QueryTable> | |||
| </Collapse> | |||
| {visible && ( | |||
| <ParameterDialog | |||
| hideModal={hideModal} | |||
| initialValue={currentRecord} | |||
| otherThanCurrentQuery={otherThanCurrentQuery} | |||
| submit={ok} | |||
| ></ParameterDialog> | |||
| )} | |||
| </Form> | |||
| </section> | |||
| ); | |||
| }; | |||
| export default UserFillUpForm; | |||
| @@ -0,0 +1,21 @@ | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { initialUserFillUpValues } from '../../constant'; | |||
| import { buildBeginInputListFromObject } from '../begin-form/utils'; | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| const values = useMemo(() => { | |||
| const formData = node?.data?.form; | |||
| if (isEmpty(formData)) { | |||
| return initialUserFillUpValues; | |||
| } | |||
| const inputs = buildBeginInputListFromObject(formData?.inputs); | |||
| return { ...(formData || {}), inputs }; | |||
| }, [node?.data?.form]); | |||
| return values; | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| import { omit } from 'lodash'; | |||
| import { useEffect } from 'react'; | |||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||
| import { BeginQuery } from '../../interface'; | |||
| import useGraphStore from '../../store'; | |||
| function transferInputsArrayToObject(inputs: BeginQuery[] = []) { | |||
| return inputs.reduce<Record<string, Omit<BeginQuery, 'key'>>>((pre, cur) => { | |||
| pre[cur.key] = omit(cur, 'key'); | |||
| return pre; | |||
| }, {}); | |||
| } | |||
| export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||
| let values = useWatch({ control: form?.control }); | |||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||
| useEffect(() => { | |||
| if (id && form?.formState.isDirty) { | |||
| values = form?.getValues(); | |||
| const nextValues = { | |||
| ...values, | |||
| inputs: transferInputsArrayToObject(values.inputs), | |||
| }; | |||
| updateNodeForm(id, nextValues); | |||
| } | |||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||
| } | |||
| @@ -43,6 +43,7 @@ import { | |||
| initialTavilyValues, | |||
| initialTemplateValues, | |||
| initialTuShareValues, | |||
| initialUserFillUpValues, | |||
| initialWaitingDialogueValues, | |||
| initialWenCaiValues, | |||
| initialWikipediaValues, | |||
| @@ -106,6 +107,7 @@ export const useInitializeOperatorParams = () => { | |||
| [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, | |||
| [Operator.Tool]: {}, | |||
| [Operator.TavilySearch]: initialTavilyValues, | |||
| [Operator.UserFillUp]: initialUserFillUpValues, | |||
| }; | |||
| }, [llmId]); | |||
| @@ -1,6 +1,6 @@ | |||
| import { IconFont } from '@/components/icon-font'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { CirclePlay } from 'lucide-react'; | |||
| import { CirclePlay, MessageSquareMore } from 'lucide-react'; | |||
| import { Operator } from './constant'; | |||
| interface IProps { | |||
| @@ -19,6 +19,7 @@ export const OperatorIconMap = { | |||
| [Operator.Switch]: 'condition', | |||
| [Operator.Code]: 'code-set', | |||
| [Operator.Agent]: 'agent-ai', | |||
| [Operator.UserFillUp]: MessageSquareMore, | |||
| // [Operator.Relevant]: BranchesOutlined, | |||
| // [Operator.RewriteQuestion]: FormOutlined, | |||
| // [Operator.KeywordExtract]: KeywordIcon, | |||