Feat: Use shadcn-ui to build GenerateForm. #3221 (#5449)

### 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)
This commit is contained in:
balibabu 2025-02-27 18:13:41 +08:00 committed by GitHub
parent 651422127c
commit 244cf49ba4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 142 additions and 249 deletions

View File

@ -1,5 +1,14 @@
import { Form, InputNumber } from 'antd';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from './ui/form';
import { Input } from './ui/input';
const MessageHistoryWindowSizeItem = ({
initialValue,
@ -21,3 +30,24 @@ const 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>
)}
/>
);
}

View File

@ -52,10 +52,12 @@ export default SimilaritySlider;
interface SimilaritySliderFormFieldProps {
vectorSimilarityWeightName?: string;
isTooltipShown?: boolean;
}
export function SimilaritySliderFormField({
vectorSimilarityWeightName = 'vector_similarity_weight',
isTooltipShown,
}: SimilaritySliderFormFieldProps) {
const form = useFormContext();
const { t } = useTranslate('knowledgeDetails');
@ -67,7 +69,9 @@ export function SimilaritySliderFormField({
name={'similarity_threshold'}
render={({ field }) => (
<FormItem>
<FormLabel>{t('similarityThreshold')}</FormLabel>
<FormLabel tooltip={isTooltipShown && t('similarityThresholdTip')}>
{t('similarityThreshold')}
</FormLabel>
<FormControl>
<SingleFormSlider
{...field}
@ -84,7 +88,11 @@ export function SimilaritySliderFormField({
name={vectorSimilarityWeightName}
render={({ field }) => (
<FormItem>
<FormLabel>{t('vectorSimilarityWeight')}</FormLabel>
<FormLabel
tooltip={isTooltipShown && t('vectorSimilarityWeightTip')}
>
{t('vectorSimilarityWeight')}
</FormLabel>
<FormControl>
<SingleFormSlider
{...field}

View File

@ -14,6 +14,8 @@ import {
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import { Info } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
const Form = FormProvider;
@ -88,17 +90,31 @@ FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
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();
return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className)}
className={cn(error && 'text-destructive', className, 'flex')}
htmlFor={formItemId}
{...props}
/>
>
{props.children}
{tooltip && (
<Tooltip>
<TooltipTrigger>
<Info className="size-3 ml-2" />
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
)}
</Label>
);
});
FormLabel.displayName = 'FormLabel';

View File

@ -122,7 +122,7 @@ const FormSheet = ({
<span>{t(`${lowerFirst(operatorName)}Description`)}</span>
</section>
</SheetHeader>
<section>
<section className="pt-4">
{visible && (
<FlowFormContext.Provider value={node}>
<OperatorForm

View File

@ -82,8 +82,15 @@ export function useFormConfigMap() {
},
[Operator.Generate]: {
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]: {
component: AnswerForm,

View File

@ -121,7 +121,7 @@ export function DynamicInputVariable({ node }: IProps) {
const { t } = useTranslation();
return (
<Collapsible defaultOpen className="group/collapsible pt-4">
<Collapsible defaultOpen className="group/collapsible">
<CollapsibleTrigger className="flex justify-between w-full pb-2">
<span className="font-bold text-2xl text-colors-text-neutral-strong">
{t('flow.input')}

View File

@ -1,101 +0,0 @@
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;

View File

@ -1,70 +0,0 @@
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,
};
};

View File

@ -1,21 +0,0 @@
.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;
}
}
}

View File

@ -1,55 +1,76 @@
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 { 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 (
<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>
);
};

View File

@ -26,7 +26,10 @@ const RetrievalForm = ({ form, node }: INextOperatorForm) => {
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<SimilaritySliderFormField vectorSimilarityWeightName="keywords_similarity_weight"></SimilaritySliderFormField>
<SimilaritySliderFormField
vectorSimilarityWeightName="keywords_similarity_weight"
isTooltipShown
></SimilaritySliderFormField>
<TopNFormField></TopNFormField>
<RerankFormFields></RerankFormFields>
<KnowledgeBaseFormField></KnowledgeBaseFormField>