Browse Source

Feat: Render DynamicCategorize with shadcn-ui. #3221 (#5610)

### What problem does this PR solve?

Feat: Render DynamicCategorize with shadcn-ui. #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.17.1
balibabu 8 months ago
parent
commit
850e218051
No account linked to committer's email address

+ 31
- 0
web/src/components/large-model-form-field.tsx View File

@@ -0,0 +1,31 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { NextLLMSelect } from './llm-select';

export function LargeModelFormField() {
const form = useFormContext();
const { t } = useTranslation();

return (
<FormField
control={form.control}
name="llm_id"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>{t('chat.model')}</FormLabel>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

+ 0
- 208
web/src/pages/agent/form-sheet/index.tsx View File

@@ -1,208 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Flex, Form, Input } from 'antd';
import { get, isPlainObject, lowerFirst } from 'lodash';
import { Play } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { BeginId, Operator, operatorMap } from '../constant';
import AkShareForm from '../form/akshare-form';
import AnswerForm from '../form/answer-form';
import ArXivForm from '../form/arxiv-form';
import BaiduFanyiForm from '../form/baidu-fanyi-form';
import BaiduForm from '../form/baidu-form';
import BeginForm from '../form/begin-form';
import BingForm from '../form/bing-form';
import CategorizeForm from '../form/categorize-form';
import CrawlerForm from '../form/crawler-form';
import DeepLForm from '../form/deepl-form';
import DuckDuckGoForm from '../form/duckduckgo-form';
import EmailForm from '../form/email-form';
import ExeSQLForm from '../form/exesql-form';
import GenerateForm from '../form/generate-form';
import GithubForm from '../form/github-form';
import GoogleForm from '../form/google-form';
import GoogleScholarForm from '../form/google-scholar-form';
import InvokeForm from '../form/invoke-form';
import Jin10Form from '../form/jin10-form';
import KeywordExtractForm from '../form/keyword-extract-form';
import MessageForm from '../form/message-form';
import PubMedForm from '../form/pubmed-form';
import QWeatherForm from '../form/qweather-form';
import RelevantForm from '../form/relevant-form';
import RetrievalForm from '../form/retrieval-form/next';
import RewriteQuestionForm from '../form/rewrite-question-form';
import SwitchForm from '../form/switch-form';
import TemplateForm from '../form/template-form';
import TuShareForm from '../form/tushare-form';
import WenCaiForm from '../form/wencai-form';
import WikipediaForm from '../form/wikipedia-form';
import YahooFinanceForm from '../form/yahoo-finance-form';
import { useHandleFormValuesChange, useHandleNodeNameChange } from '../hooks';
import OperatorIcon from '../operator-icon';
import {
buildCategorizeListFromObject,
needsSingleStepDebugging,
} from '../utils';

import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { FlowFormContext } from '../context';
import { RunTooltip } from '../flow-tooltip';
import IterationForm from '../form/iteration-from';

import styles from './index.less';

interface IProps {
node?: RAGFlowNodeType;
singleDebugDrawerVisible: IModalProps<any>['visible'];
hideSingleDebugDrawer: IModalProps<any>['hideModal'];
showSingleDebugDrawer: IModalProps<any>['showModal'];
}

const FormMap = {
[Operator.Begin]: BeginForm,
[Operator.Retrieval]: RetrievalForm,
[Operator.Generate]: GenerateForm,
[Operator.Answer]: AnswerForm,
[Operator.Categorize]: CategorizeForm,
[Operator.Message]: MessageForm,
[Operator.Relevant]: RelevantForm,
[Operator.RewriteQuestion]: RewriteQuestionForm,
[Operator.Baidu]: BaiduForm,
[Operator.DuckDuckGo]: DuckDuckGoForm,
[Operator.KeywordExtract]: KeywordExtractForm,
[Operator.Wikipedia]: WikipediaForm,
[Operator.PubMed]: PubMedForm,
[Operator.ArXiv]: ArXivForm,
[Operator.Google]: GoogleForm,
[Operator.Bing]: BingForm,
[Operator.GoogleScholar]: GoogleScholarForm,
[Operator.DeepL]: DeepLForm,
[Operator.GitHub]: GithubForm,
[Operator.BaiduFanyi]: BaiduFanyiForm,
[Operator.QWeather]: QWeatherForm,
[Operator.ExeSQL]: ExeSQLForm,
[Operator.Switch]: SwitchForm,
[Operator.WenCai]: WenCaiForm,
[Operator.AkShare]: AkShareForm,
[Operator.YahooFinance]: YahooFinanceForm,
[Operator.Jin10]: Jin10Form,
[Operator.TuShare]: TuShareForm,
[Operator.Crawler]: CrawlerForm,
[Operator.Invoke]: InvokeForm,
[Operator.Concentrator]: () => <></>,
[Operator.Note]: () => <></>,
[Operator.Template]: TemplateForm,
[Operator.Email]: EmailForm,
[Operator.Iteration]: IterationForm,
[Operator.IterationStart]: () => <></>,
};

const EmptyContent = () => <div></div>;

const FormDrawer = ({
visible,
hideModal,
node,
singleDebugDrawerVisible,
hideSingleDebugDrawer,
showSingleDebugDrawer,
}: IModalProps<any> & IProps) => {
const operatorName: Operator = node?.data.label as Operator;
const OperatorForm = FormMap[operatorName] ?? EmptyContent;
const [form] = Form.useForm();
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
id: node?.id,
data: node?.data,
});
const previousId = useRef<string | undefined>(node?.id);

const { t } = useTranslate('flow');

const { handleValuesChange } = useHandleFormValuesChange(node?.id);

useEffect(() => {
if (visible) {
if (node?.id !== previousId.current) {
form.resetFields();
}

if (operatorName === Operator.Categorize) {
const items = buildCategorizeListFromObject(
get(node, 'data.form.category_description', {}),
);
const formData = node?.data?.form;
if (isPlainObject(formData)) {
form.setFieldsValue({ ...formData, items });
}
} else {
form.setFieldsValue(node?.data?.form);
}
previousId.current = node?.id;
}
}, [visible, form, node?.data?.form, node?.id, node, operatorName]);

return (
<Sheet onOpenChange={hideModal} open={visible}>
<SheetContent>
<SheetHeader>
<Flex vertical>
<Flex gap={'middle'} align="center">
<OperatorIcon
name={operatorName}
color={operatorMap[operatorName]?.color}
></OperatorIcon>
<Flex align="center" gap={'small'} flex={1}>
<label htmlFor="" className={styles.title}>
{t('title')}
</label>
{node?.id === BeginId ? (
<span>{t(BeginId)}</span>
) : (
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</Flex>

{needsSingleStepDebugging(operatorName) && (
<RunTooltip>
<Play
className="size-5 cursor-pointer"
onClick={showSingleDebugDrawer}
/>
</RunTooltip>
)}
{/* <CloseOutlined onClick={hideModal} /> */}
</Flex>
<span className={styles.operatorDescription}>
{t(`${lowerFirst(operatorName)}Description`)}
</span>
</Flex>
</SheetHeader>
<section className={styles.formWrapper}>
{visible && (
<FlowFormContext.Provider value={node}>
<OperatorForm
onValuesChange={handleValuesChange}
form={form}
node={node}
></OperatorForm>
</FlowFormContext.Provider>
)}
</section>
</SheetContent>
{/* {singleDebugDrawerVisible && (
<SingleDebugDrawer
visible={singleDebugDrawerVisible}
hideModal={hideSingleDebugDrawer}
componentId={node?.id}
></SingleDebugDrawer>
)} */}
</Sheet>
);
};

export default FormDrawer;

+ 3
- 2
web/src/pages/agent/form-sheet/next.tsx View File

@@ -8,6 +8,7 @@ import {
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { get, isPlainObject, lowerFirst } from 'lodash';
import { Play, X } from 'lucide-react';
@@ -94,9 +95,9 @@ const FormSheet = ({
return (
<Sheet open={visible} modal={false}>
<SheetTitle className="hidden"></SheetTitle>
<SheetContent className="bg-white top-20" closeIcon={false}>
<SheetContent className={cn('bg-white top-20 p-0')} closeIcon={false}>
<SheetHeader>
<section className="flex-col border-b pb-2">
<section className="flex-col border-b py-2 px-5">
<div className="flex items-center gap-2 pb-3">
<OperatorIcon
name={operatorName}

+ 9
- 2
web/src/pages/agent/form-sheet/use-form-config-map.tsx View File

@@ -99,8 +99,15 @@ export function useFormConfigMap() {
},
[Operator.Categorize]: {
component: CategorizeForm,
defaultValues: {},
schema: z.object({}),
defaultValues: { message_history_window_size: 1 },
schema: z.object({
message_history_window_size: z.number(),
items: z.array(
z.object({
name: z.string().min(1, t('flow.nameMessage')).trim(),
}),
),
}),
},
[Operator.Message]: {
component: MessageForm,

+ 171
- 131
web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx View File

@@ -1,18 +1,25 @@
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useTranslate } from '@/hooks/common-hooks';
import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@ant-design/icons';
import { useUpdateNodeInternals } from '@xyflow/react';
import {
Button,
Collapse,
Flex,
Form,
FormListFieldData,
Input,
Select,
} from 'antd';
import { FormInstance } from 'antd/lib';
import { humanId } from 'human-id';
import humanId from 'human-id';
import trim from 'lodash/trim';
import { ChevronsUpDown, X } from 'lucide-react';
import {
ChangeEventHandler,
FocusEventHandler,
@@ -20,11 +27,10 @@ import {
useEffect,
useState,
} from 'react';
import { UseFormReturn, useFieldArray, useFormContext } from 'react-hook-form';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';

import styles from './index.less';

interface IProps {
nodeId?: string;
}
@@ -33,20 +39,20 @@ interface INameInputProps {
value?: string;
onChange?: (value: string) => void;
otherNames?: string[];
validate(errors: string[]): void;
validate(error?: string): void;
}

const getOtherFieldValues = (
form: FormInstance,
form: UseFormReturn,
formListName: string = 'items',
field: FormListFieldData,
index: number,
latestField: string,
) =>
(form.getFieldValue([formListName]) ?? [])
(form.getValues(formListName) ?? [])
.map((x: any) => x[latestField])
.filter(
(x: string) =>
x !== form.getFieldValue([formListName, field.name, latestField]),
x !== form.getValues(`${formListName}.${index}.${latestField}`),
);

const NameInput = ({
@@ -61,15 +67,16 @@ const NameInput = ({
const handleNameChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
const val = e.target.value;
setName(val);
const trimmedVal = trim(val);
// trigger validation
if (otherNames?.some((x) => x === val)) {
validate([t('nameRepeatedMsg')]);
} else if (trim(val) === '') {
validate([t('nameRequiredMsg')]);
if (otherNames?.some((x) => x === trimmedVal)) {
validate(t('nameRepeatedMsg'));
} else if (trimmedVal === '') {
validate(t('nameRequiredMsg'));
} else {
validate([]);
validate('');
}
setName(val);
},
[otherNames, validate, t],
);
@@ -97,128 +104,161 @@ const NameInput = ({
);
};

const FormSet = ({ nodeId, field }: IProps & { field: FormListFieldData }) => {
const form = Form.useFormInstance();
const FormSet = ({ nodeId, index }: IProps & { index: number }) => {
const form = useFormContext();
const { t } = useTranslate('flow');
const buildCategorizeToOptions = useBuildFormSelectOptions(
Operator.Categorize,
nodeId,
);

const buildFieldName = useCallback(
(name: string) => {
return `items.${index}.${name}`;
},
[index],
);

return (
<section>
<Form.Item
label={t('categoryName')}
name={[field.name, 'name']}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
whitespace: true,
message: t('nameMessage'),
},
]}
>
<NameInput
otherNames={getOtherFieldValues(form, 'items', field, 'name')}
validate={(errors: string[]) =>
form.setFields([
{
name: ['items', field.name, 'name'],
errors,
},
])
}
></NameInput>
</Form.Item>
<Form.Item label={t('description')} name={[field.name, 'description']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label={t('examples')} name={[field.name, 'examples']}>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item label={t('nextStep')} name={[field.name, 'to']}>
<Select
allowClear
options={buildCategorizeToOptions(
getOtherFieldValues(form, 'items', field, 'to'),
)}
/>
</Form.Item>
<Form.Item hidden name={[field.name, 'index']}>
<Input />
</Form.Item>
<section className="space-y-4">
<FormField
control={form.control}
name={buildFieldName('name')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('categoryName')}</FormLabel>
<FormControl>
<NameInput
{...field}
otherNames={getOtherFieldValues(form, 'items', index, 'name')}
validate={(error?: string) => {
const fieldName = buildFieldName('name');
if (error) {
form.setError(fieldName, { message: error });
} else {
form.clearErrors(fieldName);
}
}}
></NameInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={buildFieldName('description')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('description')}</FormLabel>
<FormControl>
<Textarea {...field} rows={3} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={buildFieldName('examples')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('examples')}</FormLabel>
<FormControl>
<Textarea {...field} rows={3} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={buildFieldName('to')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('nextStep')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
allowClear
options={buildCategorizeToOptions(
getOtherFieldValues(form, 'items', index, 'to'),
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="index"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t('examples')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</section>
);
};

const DynamicCategorize = ({ nodeId }: IProps) => {
const updateNodeInternals = useUpdateNodeInternals();
const form = Form.useFormInstance();

const form = useFormContext();
const { t } = useTranslate('flow');
const { fields, remove, append } = useFieldArray({
name: 'items',
control: form.control,
});

const handleAdd = () => {
append({
name: humanId(),
});
if (nodeId) updateNodeInternals(nodeId);
};

return (
<>
<Form.List name="items">
{(fields, { add, remove }) => {
const handleAdd = () => {
const idx = form.getFieldValue([
'items',
fields.at(-1)?.name,
'index',
]);
add({
name: humanId(),
index: fields.length === 0 ? 0 : idx + 1,
});
if (nodeId) updateNodeInternals(nodeId);
};

return (
<Flex gap={18} vertical>
{fields.map((field) => (
<Collapse
size="small"
key={field.key}
className={styles.caseCard}
items={[
{
key: field.key,
label: (
<div className="flex justify-between">
<span>
{form.getFieldValue(['items', field.name, 'name'])}
</span>
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
</div>
),
children: (
<FormSet nodeId={nodeId} field={field}></FormSet>
),
},
]}
></Collapse>
))}

<Button
type="dashed"
onClick={handleAdd}
block
className={styles.addButton}
icon={<PlusOutlined />}
>
{t('addCategory')}
</Button>
</Flex>
);
}}
</Form.List>
</>
<div className="flex flex-col gap-4 ">
{fields.map((field, index) => (
<Collapsible key={field.id}>
<div className="flex items-center justify-between space-x-4">
<h4 className="font-bold">
{form.getValues(`items.${index}.name`)}
</h4>
<CollapsibleTrigger asChild>
<div className="flex gap-4">
<Button
variant="ghost"
size="sm"
className="w-9 p-0"
onClick={() => remove(index)}
>
<X className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</div>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<FormSet nodeId={nodeId} index={index}></FormSet>
</CollapsibleContent>
</Collapsible>
))}

<Button type={'button'} onClick={handleAdd}>
<PlusOutlined />
{t('addCategory')}
</Button>
</div>
);
};


+ 0
- 13
web/src/pages/agent/form/categorize-form/index.less View File

@@ -1,13 +0,0 @@
@lightBackgroundColor: rgba(150, 150, 150, 0.07);
@darkBackgroundColor: rgba(150, 150, 150, 0.12);

.caseCard {
:global(.ant-collapse-content) {
background-color: @darkBackgroundColor;
}
}

.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

+ 23
- 31
web/src/pages/agent/form/categorize-form/index.tsx View File

@@ -1,41 +1,33 @@
import LLMSelect from '@/components/llm-select';
import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form } from 'antd';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { Form } from '@/components/ui/form';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
import DynamicCategorize from './dynamic-categorize';
import { useHandleFormValuesChange } from './hooks';

const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const { handleValuesChange } = useHandleFormValuesChange({
form,
nodeId: node?.id,
onValuesChange,
});
const CategorizeForm = ({ form, node }: INextOperatorForm) => {
// const { handleValuesChange } = useHandleFormValuesChange({
// form,
// nodeId: node?.id,
// onValuesChange,
// });

return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={handleValuesChange}
initialValues={{ items: [{}] }}
layout={'vertical'}
// onValuesChange={handleValuesChange}
{...form}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
<form
className="space-y-6 p-5 overflow-auto max-h-[76vh]"
onSubmit={(e) => {
e.preventDefault();
}}
>
<LLMSelect></LLMSelect>
</Form.Item>
<MessageHistoryWindowSizeItem
initialValue={1}
></MessageHistoryWindowSizeItem>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
<DynamicInputVariable node={node}></DynamicInputVariable>
<LargeModelFormField></LargeModelFormField>
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
</form>
</Form>
);
};

+ 0
- 16
web/src/pages/agent/form/message-form/index.less View File

@@ -1,16 +0,0 @@
.dynamicDeleteButton {
position: relative;
top: 4px;
margin: 0 8px;
color: #999;
font-size: 24px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #777;
}
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
}

Loading…
Cancel
Save