### What problem does this PR solve? Feat: Use shadcn-ui to build GenerateForm. #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.17.0
| import { Form, InputNumber } from 'antd'; | import { Form, InputNumber } from 'antd'; | ||||
| import { useFormContext } from 'react-hook-form'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from './ui/form'; | |||||
| import { Input } from './ui/input'; | |||||
| const MessageHistoryWindowSizeItem = ({ | const MessageHistoryWindowSizeItem = ({ | ||||
| initialValue, | initialValue, | ||||
| }; | }; | ||||
| export default MessageHistoryWindowSizeItem; | export default MessageHistoryWindowSizeItem; | ||||
| export function MessageHistoryWindowSizeFormField() { | |||||
| const form = useFormContext(); | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={'message_history_window_size'} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>{t('flow.messageHistoryWindowSize')}</FormLabel> | |||||
| <FormControl> | |||||
| <Input {...field} type={'number'}></Input> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| } |
| interface SimilaritySliderFormFieldProps { | interface SimilaritySliderFormFieldProps { | ||||
| vectorSimilarityWeightName?: string; | vectorSimilarityWeightName?: string; | ||||
| isTooltipShown?: boolean; | |||||
| } | } | ||||
| export function SimilaritySliderFormField({ | export function SimilaritySliderFormField({ | ||||
| vectorSimilarityWeightName = 'vector_similarity_weight', | vectorSimilarityWeightName = 'vector_similarity_weight', | ||||
| isTooltipShown, | |||||
| }: SimilaritySliderFormFieldProps) { | }: SimilaritySliderFormFieldProps) { | ||||
| const form = useFormContext(); | const form = useFormContext(); | ||||
| const { t } = useTranslate('knowledgeDetails'); | const { t } = useTranslate('knowledgeDetails'); | ||||
| name={'similarity_threshold'} | name={'similarity_threshold'} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | <FormItem> | ||||
| <FormLabel>{t('similarityThreshold')}</FormLabel> | |||||
| <FormLabel tooltip={isTooltipShown && t('similarityThresholdTip')}> | |||||
| {t('similarityThreshold')} | |||||
| </FormLabel> | |||||
| <FormControl> | <FormControl> | ||||
| <SingleFormSlider | <SingleFormSlider | ||||
| {...field} | {...field} | ||||
| name={vectorSimilarityWeightName} | name={vectorSimilarityWeightName} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | <FormItem> | ||||
| <FormLabel>{t('vectorSimilarityWeight')}</FormLabel> | |||||
| <FormLabel | |||||
| tooltip={isTooltipShown && t('vectorSimilarityWeightTip')} | |||||
| > | |||||
| {t('vectorSimilarityWeight')} | |||||
| </FormLabel> | |||||
| <FormControl> | <FormControl> | ||||
| <SingleFormSlider | <SingleFormSlider | ||||
| {...field} | {...field} |
| import { Label } from '@/components/ui/label'; | import { Label } from '@/components/ui/label'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { Info } from 'lucide-react'; | |||||
| import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip'; | |||||
| const Form = FormProvider; | const Form = FormProvider; | ||||
| const FormLabel = React.forwardRef< | const FormLabel = React.forwardRef< | ||||
| React.ElementRef<typeof LabelPrimitive.Root>, | React.ElementRef<typeof LabelPrimitive.Root>, | ||||
| React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> | |||||
| >(({ className, ...props }, ref) => { | |||||
| React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & { | |||||
| tooltip?: React.ReactNode; | |||||
| } | |||||
| >(({ className, tooltip, ...props }, ref) => { | |||||
| const { error, formItemId } = useFormField(); | const { error, formItemId } = useFormField(); | ||||
| return ( | return ( | ||||
| <Label | <Label | ||||
| ref={ref} | ref={ref} | ||||
| className={cn(error && 'text-destructive', className)} | |||||
| className={cn(error && 'text-destructive', className, 'flex')} | |||||
| htmlFor={formItemId} | htmlFor={formItemId} | ||||
| {...props} | {...props} | ||||
| /> | |||||
| > | |||||
| {props.children} | |||||
| {tooltip && ( | |||||
| <Tooltip> | |||||
| <TooltipTrigger> | |||||
| <Info className="size-3 ml-2" /> | |||||
| </TooltipTrigger> | |||||
| <TooltipContent> | |||||
| <p>{tooltip}</p> | |||||
| </TooltipContent> | |||||
| </Tooltip> | |||||
| )} | |||||
| </Label> | |||||
| ); | ); | ||||
| }); | }); | ||||
| FormLabel.displayName = 'FormLabel'; | FormLabel.displayName = 'FormLabel'; |
| <span>{t(`${lowerFirst(operatorName)}Description`)}</span> | <span>{t(`${lowerFirst(operatorName)}Description`)}</span> | ||||
| </section> | </section> | ||||
| </SheetHeader> | </SheetHeader> | ||||
| <section> | |||||
| <section className="pt-4"> | |||||
| {visible && ( | {visible && ( | ||||
| <FlowFormContext.Provider value={node}> | <FlowFormContext.Provider value={node}> | ||||
| <OperatorForm | <OperatorForm |
| }, | }, | ||||
| [Operator.Generate]: { | [Operator.Generate]: { | ||||
| component: GenerateForm, | component: GenerateForm, | ||||
| defaultValues: {}, | |||||
| schema: z.object({}), | |||||
| defaultValues: { | |||||
| cite: true, | |||||
| prompt: t('flow.promptText'), | |||||
| }, | |||||
| schema: z.object({ | |||||
| prompt: z.string().min(1, { | |||||
| message: t('flow.promptMessage'), | |||||
| }), | |||||
| }), | |||||
| }, | }, | ||||
| [Operator.Answer]: { | [Operator.Answer]: { | ||||
| component: AnswerForm, | component: AnswerForm, |
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| return ( | return ( | ||||
| <Collapsible defaultOpen className="group/collapsible pt-4"> | |||||
| <Collapsible defaultOpen className="group/collapsible"> | |||||
| <CollapsibleTrigger className="flex justify-between w-full pb-2"> | <CollapsibleTrigger className="flex justify-between w-full pb-2"> | ||||
| <span className="font-bold text-2xl text-colors-text-neutral-strong"> | <span className="font-bold text-2xl text-colors-text-neutral-strong"> | ||||
| {t('flow.input')} | {t('flow.input')} |
| import { EditableCell, EditableRow } from '@/components/editable-cell'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||||
| import { DeleteOutlined } from '@ant-design/icons'; | |||||
| import { Button, Flex, Select, Table, TableProps } from 'antd'; | |||||
| import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||||
| import { IGenerateParameter } from '../../interface'; | |||||
| import { useHandleOperateParameters } from './hooks'; | |||||
| import styles from './index.less'; | |||||
| interface IProps { | |||||
| node?: RAGFlowNodeType; | |||||
| } | |||||
| const components = { | |||||
| body: { | |||||
| row: EditableRow, | |||||
| cell: EditableCell, | |||||
| }, | |||||
| }; | |||||
| const DynamicParameters = ({ node }: IProps) => { | |||||
| const nodeId = node?.id; | |||||
| const { t } = useTranslate('flow'); | |||||
| const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); | |||||
| const { | |||||
| dataSource, | |||||
| handleAdd, | |||||
| handleRemove, | |||||
| handleSave, | |||||
| handleComponentIdChange, | |||||
| } = useHandleOperateParameters(nodeId!); | |||||
| const columns: TableProps<IGenerateParameter>['columns'] = [ | |||||
| { | |||||
| title: t('key'), | |||||
| dataIndex: 'key', | |||||
| key: 'key', | |||||
| width: '40%', | |||||
| onCell: (record: IGenerateParameter) => ({ | |||||
| record, | |||||
| editable: true, | |||||
| dataIndex: 'key', | |||||
| title: 'key', | |||||
| handleSave, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: t('value'), | |||||
| dataIndex: 'component_id', | |||||
| key: 'component_id', | |||||
| align: 'center', | |||||
| width: '40%', | |||||
| render(text, record) { | |||||
| return ( | |||||
| <Select | |||||
| style={{ width: '100%' }} | |||||
| allowClear | |||||
| options={options} | |||||
| value={text} | |||||
| onChange={handleComponentIdChange(record)} | |||||
| /> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: t('operation'), | |||||
| dataIndex: 'operation', | |||||
| width: 20, | |||||
| key: 'operation', | |||||
| align: 'center', | |||||
| fixed: 'right', | |||||
| render(_, record) { | |||||
| return <DeleteOutlined onClick={handleRemove(record.id)} />; | |||||
| }, | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <section> | |||||
| <Flex justify="end"> | |||||
| <Button size="small" onClick={handleAdd}> | |||||
| {t('add')} | |||||
| </Button> | |||||
| </Flex> | |||||
| <Table | |||||
| dataSource={dataSource} | |||||
| columns={columns} | |||||
| rowKey={'id'} | |||||
| className={styles.variableTable} | |||||
| components={components} | |||||
| rowClassName={() => styles.editableRow} | |||||
| scroll={{ x: true }} | |||||
| bordered | |||||
| /> | |||||
| </section> | |||||
| ); | |||||
| }; | |||||
| export default DynamicParameters; |
| import get from 'lodash/get'; | |||||
| import { useCallback, useMemo } from 'react'; | |||||
| import { v4 as uuid } from 'uuid'; | |||||
| import { IGenerateParameter } from '../../interface'; | |||||
| import useGraphStore from '../../store'; | |||||
| export const useHandleOperateParameters = (nodeId: string) => { | |||||
| const { getNode, updateNodeForm } = useGraphStore((state) => state); | |||||
| const node = getNode(nodeId); | |||||
| const dataSource: IGenerateParameter[] = useMemo( | |||||
| () => get(node, 'data.form.parameters', []) as IGenerateParameter[], | |||||
| [node], | |||||
| ); | |||||
| const handleComponentIdChange = useCallback( | |||||
| (row: IGenerateParameter) => (value: string) => { | |||||
| const newData = [...dataSource]; | |||||
| const index = newData.findIndex((item) => row.id === item.id); | |||||
| const item = newData[index]; | |||||
| newData.splice(index, 1, { | |||||
| ...item, | |||||
| component_id: value, | |||||
| }); | |||||
| updateNodeForm(nodeId, { parameters: newData }); | |||||
| }, | |||||
| [updateNodeForm, nodeId, dataSource], | |||||
| ); | |||||
| const handleRemove = useCallback( | |||||
| (id?: string) => () => { | |||||
| const newData = dataSource.filter((item) => item.id !== id); | |||||
| updateNodeForm(nodeId, { parameters: newData }); | |||||
| }, | |||||
| [updateNodeForm, nodeId, dataSource], | |||||
| ); | |||||
| const handleAdd = useCallback(() => { | |||||
| updateNodeForm(nodeId, { | |||||
| parameters: [ | |||||
| ...dataSource, | |||||
| { | |||||
| id: uuid(), | |||||
| key: '', | |||||
| component_id: undefined, | |||||
| }, | |||||
| ], | |||||
| }); | |||||
| }, [dataSource, nodeId, updateNodeForm]); | |||||
| const handleSave = (row: IGenerateParameter) => { | |||||
| const newData = [...dataSource]; | |||||
| const index = newData.findIndex((item) => row.id === item.id); | |||||
| const item = newData[index]; | |||||
| newData.splice(index, 1, { | |||||
| ...item, | |||||
| ...row, | |||||
| }); | |||||
| updateNodeForm(nodeId, { parameters: newData }); | |||||
| }; | |||||
| return { | |||||
| handleAdd, | |||||
| handleRemove, | |||||
| handleComponentIdChange, | |||||
| handleSave, | |||||
| dataSource, | |||||
| }; | |||||
| }; |
| .variableTable { | |||||
| margin-top: 14px; | |||||
| } | |||||
| .editableRow { | |||||
| :global(.editable-cell) { | |||||
| position: relative; | |||||
| } | |||||
| :global(.editable-cell-value-wrap) { | |||||
| padding: 5px 12px; | |||||
| cursor: pointer; | |||||
| height: 30px !important; | |||||
| } | |||||
| &:hover { | |||||
| :global(.editable-cell-value-wrap) { | |||||
| padding: 4px 11px; | |||||
| border: 1px solid #d9d9d9; | |||||
| border-radius: 2px; | |||||
| } | |||||
| } | |||||
| } |
| import LLMSelect from '@/components/llm-select'; | import LLMSelect from '@/components/llm-select'; | ||||
| import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item'; | |||||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||||
| import { PromptEditor } from '@/components/prompt-editor'; | import { PromptEditor } from '@/components/prompt-editor'; | ||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { Form, Switch } from 'antd'; | |||||
| import { IOperatorForm } from '../../interface'; | |||||
| import { | |||||
| Form, | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { Switch } from '@/components/ui/switch'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { INextOperatorForm } from '../../interface'; | |||||
| const GenerateForm = ({ onValuesChange, form }: IOperatorForm) => { | |||||
| const { t } = useTranslate('flow'); | |||||
| const GenerateForm = ({ form }: INextOperatorForm) => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | return ( | ||||
| <Form | |||||
| name="basic" | |||||
| autoComplete="off" | |||||
| form={form} | |||||
| onValuesChange={onValuesChange} | |||||
| layout={'vertical'} | |||||
| > | |||||
| <Form.Item | |||||
| name={'llm_id'} | |||||
| label={t('model', { keyPrefix: 'chat' })} | |||||
| tooltip={t('modelTip', { keyPrefix: 'chat' })} | |||||
| <Form {...form}> | |||||
| <form | |||||
| className="space-y-6" | |||||
| onSubmit={(e) => { | |||||
| e.preventDefault(); | |||||
| }} | |||||
| > | > | ||||
| <LLMSelect></LLMSelect> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| name={['prompt']} | |||||
| label={t('systemPrompt')} | |||||
| initialValue={t('promptText')} | |||||
| tooltip={t('promptTip', { keyPrefix: 'knowledgeConfiguration' })} | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: t('promptMessage'), | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| {/* <Input.TextArea rows={8}></Input.TextArea> */} | |||||
| <PromptEditor></PromptEditor> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| name={['cite']} | |||||
| label={t('cite')} | |||||
| initialValue={true} | |||||
| valuePropName="checked" | |||||
| tooltip={t('citeTip')} | |||||
| > | |||||
| <Switch /> | |||||
| </Form.Item> | |||||
| <MessageHistoryWindowSizeItem | |||||
| initialValue={12} | |||||
| ></MessageHistoryWindowSizeItem> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="llm_id" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('chat.modelTip')}> | |||||
| {t('chat.model')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <LLMSelect {...field} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="prompt" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('knowledgeConfiguration.promptTip')}> | |||||
| {t('flow.systemPrompt')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <PromptEditor {...field} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="cite" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('flow.citeTip')}> | |||||
| {t('flow.cite')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <Switch {...field} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> | |||||
| </form> | |||||
| </Form> | </Form> | ||||
| ); | ); | ||||
| }; | }; |
| }} | }} | ||||
| > | > | ||||
| <DynamicInputVariable node={node}></DynamicInputVariable> | <DynamicInputVariable node={node}></DynamicInputVariable> | ||||
| <SimilaritySliderFormField vectorSimilarityWeightName="keywords_similarity_weight"></SimilaritySliderFormField> | |||||
| <SimilaritySliderFormField | |||||
| vectorSimilarityWeightName="keywords_similarity_weight" | |||||
| isTooltipShown | |||||
| ></SimilaritySliderFormField> | |||||
| <TopNFormField></TopNFormField> | <TopNFormField></TopNFormField> | ||||
| <RerankFormFields></RerankFormFields> | <RerankFormFields></RerankFormFields> | ||||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | <KnowledgeBaseFormField></KnowledgeBaseFormField> |