Features: Memory page rendering and other bug fixes (#11784)

### What problem does this PR solve?

Features: Memory page rendering and other bug fixes
- Rendering of the Memory list page
- Rendering of the message list page in Memory
- Fixed an issue where the empty state was incorrectly displayed when
search criteria were applied
- Added a web link for the API-Key
- modifying the index_mode attribute of the Confluence data source.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-12-08 10:17:56 +08:00
committed by GitHub
parent 3285f09c92
commit 660fa8888b
55 changed files with 2047 additions and 218 deletions

View File

@ -1,6 +1,13 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react';
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import {
ControllerRenderProps,
DefaultValues,
FieldValues,
SubmitHandler,
@ -26,6 +33,7 @@ import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { Loader } from 'lucide-react';
import { MultiSelect, MultiSelectOptionType } from './ui/multi-select';
// Field type enumeration
export enum FormFieldType {
@ -35,14 +43,17 @@ export enum FormFieldType {
Number = 'number',
Textarea = 'textarea',
Select = 'select',
MultiSelect = 'multi-select',
Checkbox = 'checkbox',
Tag = 'tag',
Custom = 'custom',
}
// Field configuration interface
export interface FormFieldConfig {
name: string;
label: string;
hideLabel?: boolean;
type: FormFieldType;
hidden?: boolean;
required?: boolean;
@ -57,7 +68,7 @@ export interface FormFieldConfig {
max?: number;
message?: string;
};
render?: (fieldProps: any) => React.ReactNode;
render?: (fieldProps: ControllerRenderProps) => React.ReactNode;
horizontal?: boolean;
onChange?: (value: any) => void;
tooltip?: React.ReactNode;
@ -78,10 +89,10 @@ interface DynamicFormProps<T extends FieldValues> {
className?: string;
children?: React.ReactNode;
defaultValues?: DefaultValues<T>;
onFieldUpdate?: (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => void;
// onFieldUpdate?: (
// fieldName: string,
// updatedField: Partial<FormFieldConfig>,
// ) => void;
labelClassName?: string;
}
@ -92,6 +103,10 @@ export interface DynamicFormRef {
reset: (values?: any) => void;
watch: (field: string, callback: (value: any) => void) => () => void;
updateFieldType: (fieldName: string, newType: FormFieldType) => void;
onFieldUpdate: (
fieldName: string,
newFieldProperties: Partial<FormFieldConfig>,
) => void;
}
// Generate Zod validation schema based on field configurations
@ -110,6 +125,14 @@ const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
case FormFieldType.Email:
fieldSchema = z.string().email('Please enter a valid email address');
break;
case FormFieldType.MultiSelect:
fieldSchema = z.array(z.string()).optional();
if (field.required) {
fieldSchema = z.array(z.string()).min(1, {
message: `${field.label} is required`,
});
}
break;
case FormFieldType.Number:
fieldSchema = z.coerce.number();
if (field.validation?.min !== undefined) {
@ -275,7 +298,10 @@ const generateDefaultValues = <T extends FieldValues>(
defaultValues[field.name] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
defaultValues[field.name] = false;
} else if (field.type === FormFieldType.Tag) {
} else if (
field.type === FormFieldType.Tag ||
field.type === FormFieldType.MultiSelect
) {
defaultValues[field.name] = [];
} else {
defaultValues[field.name] = '';
@ -291,17 +317,21 @@ const DynamicForm = {
Root: forwardRef(
<T extends FieldValues>(
{
fields,
fields: originFields,
onSubmit,
className = '',
children,
defaultValues: formDefaultValues = {} as DefaultValues<T>,
onFieldUpdate,
// onFieldUpdate,
labelClassName,
}: DynamicFormProps<T>,
ref: React.Ref<any>,
) => {
// Generate validation schema and default values
const [fields, setFields] = useState(originFields);
useMemo(() => {
setFields(originFields);
}, [originFields]);
const schema = useMemo(() => generateSchema(fields), [fields]);
const defaultValues = useMemo(() => {
@ -406,43 +436,54 @@ const DynamicForm = {
}, [fields, form]);
// Expose form methods via ref
useImperativeHandle(ref, () => ({
submit: () => form.handleSubmit(onSubmit)(),
getValues: () => form.getValues(),
reset: (values?: T) => {
if (values) {
form.reset(values);
} else {
form.reset();
}
},
setError: form.setError,
clearErrors: form.clearErrors,
trigger: form.trigger,
watch: (field: string, callback: (value: any) => void) => {
const { unsubscribe } = form.watch((values: any) => {
if (values && values[field] !== undefined) {
callback(values[field]);
}
});
return unsubscribe;
},
onFieldUpdate: (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => {
setTimeout(() => {
if (onFieldUpdate) {
onFieldUpdate(fieldName, updatedField);
useImperativeHandle(
ref,
() => ({
submit: () => form.handleSubmit(onSubmit)(),
getValues: () => form.getValues(),
reset: (values?: T) => {
if (values) {
form.reset(values);
} else {
console.warn(
'onFieldUpdate prop is not provided. Cannot update field type.',
);
form.reset();
}
}, 0);
},
}));
},
setError: form.setError,
clearErrors: form.clearErrors,
trigger: form.trigger,
watch: (field: string, callback: (value: any) => void) => {
const { unsubscribe } = form.watch((values: any) => {
if (values && values[field] !== undefined) {
callback(values[field]);
}
});
return unsubscribe;
},
onFieldUpdate: (
fieldName: string,
updatedField: Partial<FormFieldConfig>,
) => {
setFields((prevFields: any) =>
prevFields.map((field: any) =>
field.name === fieldName
? { ...field, ...updatedField }
: field,
),
);
// setTimeout(() => {
// if (onFieldUpdate) {
// onFieldUpdate(fieldName, updatedField);
// } else {
// console.warn(
// 'onFieldUpdate prop is not provided. Cannot update field type.',
// );
// }
// }, 0);
},
}),
[form],
);
useEffect(() => {
if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
@ -459,6 +500,9 @@ const DynamicForm = {
// Render form fields
const renderField = (field: FormFieldConfig) => {
if (field.render) {
if (field.type === FormFieldType.Custom && field.hideLabel) {
return <div className="w-full">{field.render({})}</div>;
}
return (
<RAGFlowFormItem
name={field.name}
@ -549,6 +593,43 @@ const DynamicForm = {
</RAGFlowFormItem>
);
case FormFieldType.MultiSelect:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
console.log('multi select value', fieldProps);
const finalFieldProps = {
...fieldProps,
onValueChange: (value: string[]) => {
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
};
return (
<MultiSelect
variant="inverted"
maxCount={100}
{...finalFieldProps}
// onValueChange={(data) => {
// console.log(data);
// field.onChange?.(data);
// }}
options={field.options as MultiSelectOptionType[]}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Checkbox:
return (
<FormField

View File

@ -11,23 +11,33 @@ export enum EmptyCardType {
Dataset = 'dataset',
Chat = 'chat',
Search = 'search',
Memory = 'memory',
}
export const EmptyCardData = {
[EmptyCardType.Agent]: {
icon: <HomeIcon name="agents" width={'24'} />,
title: t('empty.agentTitle'),
notFound: t('empty.notFoundAgent'),
},
[EmptyCardType.Dataset]: {
icon: <HomeIcon name="datasets" width={'24'} />,
title: t('empty.datasetTitle'),
notFound: t('empty.notFoundDataset'),
},
[EmptyCardType.Chat]: {
icon: <HomeIcon name="chats" width={'24'} />,
title: t('empty.chatTitle'),
notFound: t('empty.notFoundChat'),
},
[EmptyCardType.Search]: {
icon: <HomeIcon name="searches" width={'24'} />,
title: t('empty.searchTitle'),
notFound: t('empty.notFoundSearch'),
},
[EmptyCardType.Memory]: {
icon: <HomeIcon name="memory" width={'24'} />,
title: t('empty.memoryTitle'),
notFound: t('empty.notFoundMemory'),
},
};

View File

@ -76,9 +76,10 @@ export const EmptyAppCard = (props: {
onClick?: () => void;
showIcon?: boolean;
className?: string;
isSearch?: boolean;
size?: 'small' | 'large';
}) => {
const { type, showIcon, className } = props;
const { type, showIcon, className, isSearch } = props;
let defaultClass = '';
let style = {};
switch (props.size) {
@ -95,19 +96,29 @@ export const EmptyAppCard = (props: {
break;
}
return (
<div className=" cursor-pointer " onClick={props.onClick}>
<div
className=" cursor-pointer "
onClick={isSearch ? undefined : props.onClick}
>
<EmptyCard
icon={showIcon ? EmptyCardData[type].icon : undefined}
title={EmptyCardData[type].title}
title={
isSearch ? EmptyCardData[type].notFound : EmptyCardData[type].title
}
className={className}
style={style}
// description={EmptyCardData[type].description}
>
<div
className={cn(defaultClass, 'flex items-center justify-start w-full')}
>
<Plus size={24} />
</div>
{!isSearch && (
<div
className={cn(
defaultClass,
'flex items-center justify-start w-full',
)}
>
<Plus size={24} />
</div>
)}
</EmptyCard>
</div>
);

View File

@ -9,13 +9,19 @@ export type LLMFormFieldProps = {
name?: string;
};
export function LLMFormField({ options, name }: LLMFormFieldProps) {
const { t } = useTranslation();
export const useModelOptions = () => {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
return {
modelOptions,
};
};
export function LLMFormField({ options, name }: LLMFormFieldProps) {
const { t } = useTranslation();
const { modelOptions } = useModelOptions();
return (
<RAGFlowFormItem name={name || 'llm_id'} label={t('chat.model')}>

View File

@ -53,14 +53,16 @@ export function RAGFlowFormItem({
{label}
</FormLabel>
)}
<FormControl>
{typeof children === 'function'
? children(field)
: isValidElement(children)
? cloneElement(children, { ...field })
: children}
</FormControl>
<FormMessage />
<div className="w-full flex flex-col">
<FormControl>
{typeof children === 'function'
? children(field)
: isValidElement(children)
? cloneElement(children, { ...field })
: children}
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>