Feature: Memory interface integration testing (#11833)

### What problem does this PR solve?

Feature: Memory interface integration testing

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-12-09 14:52:58 +08:00
committed by GitHub
parent c51e6b2a58
commit 28bc87c5e2
32 changed files with 1168 additions and 501 deletions

View File

@ -45,7 +45,7 @@ export function ConfirmDeleteDialog({
const { t } = useTranslation();
if (hidden) {
return children;
return children || <></>;
}
return (
@ -54,7 +54,7 @@ export function ConfirmDeleteDialog({
open={open}
defaultOpen={defaultOpen}
>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
{children && <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>}
<AlertDialogOverlay
onClick={(e) => {
e.stopPropagation();
@ -109,23 +109,28 @@ export function ConfirmDeleteDialog({
export const ConfirmDeleteDialogNode = ({
avatar,
name,
warnText,
children,
}: {
avatar?: { avatar?: string; name?: string; isPerson?: boolean };
name?: string;
warnText?: string;
children?: React.ReactNode;
}) => {
return (
<div className="flex items-center border-0.5 text-text-secondary border-border-button rounded-lg px-3 py-4">
{avatar && (
<RAGFlowAvatar
className="w-8 h-8"
avatar={avatar.avatar}
isPerson={avatar.isPerson}
name={avatar.name}
/>
)}
{name && <div className="ml-3">{name}</div>}
<div className="flex flex-col gap-2.5">
<div className="flex items-center border-0.5 text-text-secondary border-border-button rounded-lg px-3 py-4">
{avatar && (
<RAGFlowAvatar
className="w-8 h-8"
avatar={avatar.avatar}
isPerson={avatar.isPerson}
name={avatar.name}
/>
)}
{name && <div className="ml-3">{name}</div>}
</div>
{warnText && <div className="text-state-error text-xs">{warnText}</div>}
{children}
</div>
);

View File

@ -110,7 +110,7 @@ export interface DynamicFormRef {
}
// Generate Zod validation schema based on field configurations
const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
export const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
const schema: Record<string, ZodSchema> = {};
const nestedSchemas: Record<string, Record<string, ZodSchema>> = {};
@ -311,6 +311,271 @@ const generateDefaultValues = <T extends FieldValues>(
return defaultValues as DefaultValues<T>;
};
// Render form fields
export const RenderField = ({
field,
labelClassName,
}: {
field: FormFieldConfig;
labelClassName?: string;
}) => {
const form = useFormContext();
if (field.render) {
if (field.type === FormFieldType.Custom && field.hideLabel) {
return <div className="w-full">{field.render({})}</div>;
}
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target?.value ?? e);
},
}
: fieldProps;
return field.render?.(finalFieldProps);
}}
</RAGFlowFormItem>
);
}
switch (field.type) {
case FormFieldType.Textarea:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Textarea
{...finalFieldProps}
placeholder={field.placeholder}
// className="resize-none"
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Select:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string) => {
console.log('select value', value);
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
}
: fieldProps;
return (
<SelectWithSearch
triggerClassName="!shrink"
{...finalFieldProps}
options={field.options}
/>
);
}}
</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
control={form.control}
name={field.name as any}
render={({ field: formField }) => (
<FormItem
className={cn('flex items-center w-full', {
'flex-row items-center space-x-3 space-y-0': !field.horizontal,
})}
>
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
{field.label && field.horizontal && (
<div className="space-y-1 leading-none w-1/4">
<FormLabel
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormControl>
<div className={cn({ 'w-full': field.horizontal })}>
<Checkbox
checked={formField.value}
onCheckedChange={(checked) => {
formField.onChange(checked);
field.onChange?.(checked);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
case FormFieldType.Tag:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string[]) => {
fieldProps.onChange(value);
field.onChange?.(value);
},
}
: fieldProps;
return (
// <TagInput {...fieldProps} placeholder={field.placeholder} />
<div className="w-full">
<EditTag {...finalFieldProps}></EditTag>
</div>
);
}}
</RAGFlowFormItem>
);
default:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<div className="w-full">
<Input
{...finalFieldProps}
type={field.type}
placeholder={field.placeholder}
/>
</div>
);
}}
</RAGFlowFormItem>
);
}
};
// Dynamic form component
const DynamicForm = {
@ -497,266 +762,6 @@ const DynamicForm = {
// Submit handler
// const handleSubmit = form.handleSubmit(onSubmit);
// 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}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target?.value ?? e);
},
}
: fieldProps;
return field.render?.(finalFieldProps);
}}
</RAGFlowFormItem>
);
}
switch (field.type) {
case FormFieldType.Textarea:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Textarea
{...finalFieldProps}
placeholder={field.placeholder}
// className="resize-none"
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Select:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string) => {
console.log('select value', value);
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
}
: fieldProps;
return (
<SelectWithSearch
triggerClassName="!shrink"
{...finalFieldProps}
options={field.options}
/>
);
}}
</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
control={form.control}
name={field.name as any}
render={({ field: formField }) => (
<FormItem
className={cn('flex items-center w-full', {
'flex-row items-center space-x-3 space-y-0':
!field.horizontal,
})}
>
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
{field.label && field.horizontal && (
<div className="space-y-1 leading-none w-1/4">
<FormLabel
className={cn(
'font-medium',
labelClassName || field.labelClassName,
)}
tooltip={field.tooltip}
>
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormControl>
<div className={cn({ 'w-full': field.horizontal })}>
<Checkbox
checked={formField.value}
onCheckedChange={(checked) => {
formField.onChange(checked);
field.onChange?.(checked);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
case FormFieldType.Tag:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string[]) => {
fieldProps.onChange(value);
field.onChange?.(value);
},
}
: fieldProps;
return (
// <TagInput {...fieldProps} placeholder={field.placeholder} />
<div className="w-full">
<EditTag {...finalFieldProps}></EditTag>
</div>
);
}}
</RAGFlowFormItem>
);
default:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<div className="w-full">
<Input
{...finalFieldProps}
type={field.type}
placeholder={field.placeholder}
/>
</div>
);
}}
</RAGFlowFormItem>
);
}
};
// Watch all form values to re-render when they change (for shouldRender checks)
const formValues = form.watch();
@ -779,7 +784,10 @@ const DynamicForm = {
key={field.name}
className={cn({ hidden: field.hidden || !shouldShow })}
>
{renderField(field)}
<RenderField
field={field}
labelClassName={labelClassName}
/>
</div>
);
})}
@ -798,7 +806,7 @@ const DynamicForm = {
buttonText,
submitFunc,
}: {
submitLoading: boolean;
submitLoading?: boolean;
buttonText?: string;
submitFunc?: (values: FieldValues) => void;
}) => {

View File

@ -53,7 +53,12 @@ export function RAGFlowFormItem({
{label}
</FormLabel>
)}
<div className="w-full flex flex-col">
<div
className={cn('flex flex-col', {
'w-3/4': horizontal,
'w-full': !horizontal,
})}
>
<FormControl>
{typeof children === 'function'
? children(field)

View File

@ -70,6 +70,7 @@ export function Header() {
{ path: Routes.Chats, name: t('header.chat'), icon: MessageSquareText },
{ path: Routes.Searches, name: t('header.search'), icon: Search },
{ path: Routes.Agents, name: t('header.flow'), icon: Cpu },
// { path: Routes.Memories, name: t('header.Memories'), icon: Cpu },
{ path: Routes.Files, name: t('header.fileManager'), icon: File },
],
[t],

View File

@ -101,7 +101,7 @@ export default {
dataset: 'Dataset',
Memories: 'Memory',
},
memory: {
memories: {
memory: 'Memory',
createMemory: 'Create Memory',
name: 'Name',
@ -110,9 +110,15 @@ export default {
embeddingModel: 'Embedding model',
selectModel: 'Select model',
llm: 'LLM',
delMemoryWarn: `After deletion, all messages in this memory will be deleted and cannot be retrieved by agents.`,
},
memoryDetail: {
memory: {
messages: {
copied: 'Copied!',
contentEmbed: 'Content embed',
content: 'Content',
delMessageWarn: `After forgetting, this message will not be retrieved by agents.`,
forgetMessage: 'Forget message',
sessionId: 'Session ID',
agent: 'Agent',
type: 'Type',
@ -122,6 +128,27 @@ export default {
enable: 'Enable',
action: 'Action',
},
config: {
avatar: 'Avatar',
description: 'Description',
memorySize: 'Memory size',
advancedSettings: 'Advanced Settings',
permission: 'Permission',
onlyMe: 'Only Me',
team: 'Team',
storageType: 'Storage Type',
storageTypePlaceholder: 'Please select storage type',
forgetPolicy: 'Forget Policy',
temperature: 'Temperature',
systemPrompt: 'System Prompt',
systemPromptPlaceholder: 'Please enter system prompt',
userPrompt: 'User Prompt',
userPromptPlaceholder: 'Please enter user prompt',
},
sideBar: {
messages: 'Messages',
configuration: 'Configuration',
},
},
knowledgeList: {
welcome: 'Welcome back',

View File

@ -1,4 +1,5 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import {
FormControl,
FormField,
@ -11,6 +12,7 @@ import { Spin } from '@/components/ui/spin';
import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { useMemo, useState } from 'react';
import { FieldValues, useFormContext } from 'react-hook-form';
import {
@ -285,3 +287,14 @@ export function EnableTocToggle() {
/>
);
}
export function OverlappedPercent() {
return (
<SliderInputFormField
name="parser_config.overlapped_percent"
label={t('flow.filenameEmbeddingWeight')}
max={0.5}
step={0.01}
></SliderInputFormField>
);
}

View File

@ -10,7 +10,7 @@ import {
ConfigurationFormContainer,
MainContainer,
} from '../configuration-form-container';
import { EnableTocToggle } from './common-item';
import { EnableTocToggle, OverlappedPercent } from './common-item';
export function NaiveConfiguration() {
return (
@ -20,6 +20,7 @@ export function NaiveConfiguration() {
<MaxTokenNumberFormField initialValue={512}></MaxTokenNumberFormField>
<DelimiterFormField></DelimiterFormField>
<EnableTocToggle />
<OverlappedPercent />
</ConfigurationFormContainer>
<ConfigurationFormContainer>
<AutoKeywordsFormField></AutoKeywordsFormField>

View File

@ -29,6 +29,7 @@ export const formSchema = z
tag_kb_ids: z.array(z.string()).nullish(),
topn_tags: z.number().optional(),
toc_extraction: z.boolean().optional(),
overlapped_percent: z.number().optional(),
raptor: z
.object({
use_raptor: z.boolean().optional(),

View File

@ -67,6 +67,7 @@ export default function DatasetSettings() {
html4excel: false,
topn_tags: 3,
toc_extraction: false,
overlapped_percent: 0,
raptor: {
use_raptor: true,
max_token: 256,

View File

@ -14,16 +14,18 @@ export function PermissionFormField() {
}, [t]);
return (
<RAGFlowFormItem
name="permission"
label={t('knowledgeConfiguration.permissions')}
tooltip={t('knowledgeConfiguration.permissionsTip')}
horizontal
>
<SelectWithSearch
options={teamOptions}
triggerClassName="w-3/4"
></SelectWithSearch>
</RAGFlowFormItem>
<div className="items-center">
<RAGFlowFormItem
name="permission"
label={t('knowledgeConfiguration.permissions')}
tooltip={t('knowledgeConfiguration.permissionsTip')}
horizontal={true}
>
<SelectWithSearch
options={teamOptions}
triggerClassName="w-full"
></SelectWithSearch>
</RAGFlowFormItem>
</div>
);
}

View File

@ -3,7 +3,7 @@ import { useModelOptions } from '@/components/llm-setting-items/llm-form-field';
import { HomeIcon } from '@/components/svg-icon';
import { Modal } from '@/components/ui/modal/modal';
import { t } from 'i18next';
import { useCallback, useEffect, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { createMemoryFields } from './constants';
import { IMemory } from './interface';
@ -13,11 +13,10 @@ type IProps = {
onSubmit?: (data: any) => void;
initialMemory: IMemory;
loading?: boolean;
isCreate?: boolean;
};
export const AddOrEditModal = (props: IProps) => {
const { open, onClose, onSubmit, initialMemory } = props;
// const [fields, setFields] = useState<FormFieldConfig[]>(createMemoryFields);
// const formRef = useRef<DynamicFormRef>(null);
export const AddOrEditModal = memo((props: IProps) => {
const { open, onClose, onSubmit, initialMemory, isCreate } = props;
const [formInstance, setFormInstance] = useState<DynamicFormRef | null>(null);
const formCallbackRef = useCallback((node: DynamicFormRef | null) => {
@ -28,15 +27,25 @@ export const AddOrEditModal = (props: IProps) => {
}, []);
const { modelOptions } = useModelOptions();
useEffect(() => {
if (initialMemory && initialMemory.id) {
formInstance?.onFieldUpdate('memory_type', { hidden: true });
formInstance?.onFieldUpdate('embedding', { hidden: true });
formInstance?.onFieldUpdate('llm', { hidden: true });
const fields = useMemo(() => {
if (!isCreate) {
return createMemoryFields.filter((field: any) => field.name === 'name');
} else {
formInstance?.onFieldUpdate('llm', { options: modelOptions as any });
const tempFields = createMemoryFields.map((field: any) => {
if (field.name === 'llm_id') {
return {
...field,
options: modelOptions,
};
} else {
return {
...field,
};
}
});
return tempFields;
}
}, [modelOptions, formInstance, initialMemory]);
}, [modelOptions, isCreate]);
return (
<Modal
@ -48,7 +57,7 @@ export const AddOrEditModal = (props: IProps) => {
<div>
<HomeIcon name="memory" width={'24'} />
</div>
{t('memory.createMemory')}
{t('memories.createMemory')}
</div>
}
showfooter={false}
@ -56,7 +65,7 @@ export const AddOrEditModal = (props: IProps) => {
>
<DynamicForm.Root
ref={formCallbackRef}
fields={createMemoryFields}
fields={fields}
onSubmit={() => {}}
defaultValues={initialMemory}
>
@ -72,4 +81,4 @@ export const AddOrEditModal = (props: IProps) => {
</DynamicForm.Root>
</Modal>
);
};
});

View File

@ -4,16 +4,16 @@ import { t } from 'i18next';
export const createMemoryFields = [
{
name: 'memory_name',
label: t('memory.name'),
placeholder: t('memory.memoryNamePlaceholder'),
name: 'name',
label: t('memories.name'),
placeholder: t('memories.memoryNamePlaceholder'),
required: true,
},
{
name: 'memory_type',
label: t('memory.memoryType'),
label: t('memories.memoryType'),
type: FormFieldType.MultiSelect,
placeholder: t('memory.descriptionPlaceholder'),
placeholder: t('memories.descriptionPlaceholder'),
options: [
{ label: 'Raw', value: 'raw' },
{ label: 'Semantic', value: 'semantic' },
@ -23,18 +23,18 @@ export const createMemoryFields = [
required: true,
},
{
name: 'embedding',
label: t('memory.embeddingModel'),
placeholder: t('memory.selectModel'),
name: 'embd_id',
label: t('memories.embeddingModel'),
placeholder: t('memories.selectModel'),
required: true,
// hideLabel: true,
// type: 'custom',
render: (field) => <EmbeddingSelect field={field} isEdit={false} />,
},
{
name: 'llm',
label: t('memory.llm'),
placeholder: t('memory.selectModel'),
name: 'llm_id',
label: t('memories.llm'),
placeholder: t('memories.selectModel'),
required: true,
type: FormFieldType.Select,
},

View File

@ -7,6 +7,7 @@ import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import memoryService, { updateMemoryById } from '@/services/memory-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { omit } from 'lodash';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi';
@ -73,12 +74,12 @@ export const useFetchMemoryList = () => {
queryFn: async () => {
const { data: response } = await memoryService.getMemoryList(
{
params: {
data: {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
data: {},
params: {},
},
true,
);
@ -153,7 +154,9 @@ export const useDeleteMemory = () => {
} = useMutation<DeleteMemoryResponse, Error, DeleteMemoryProps>({
mutationKey: ['deleteMemory'],
mutationFn: async (props) => {
const { data: response } = await memoryService.deleteMemory(props);
const { data: response } = await memoryService.deleteMemory(
props.memory_id,
);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to delete memory');
}
@ -189,7 +192,8 @@ export const useUpdateMemory = () => {
} = useMutation<any, Error, IMemoryAppDetailProps>({
mutationKey: ['updateMemory'],
mutationFn: async (formData) => {
const { data: response } = await updateMemoryById(formData.id, formData);
const param = omit(formData, ['id']);
const { data: response } = await updateMemoryById(formData.id, param);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to update memory');
}
@ -259,7 +263,7 @@ export const useRenameMemory = () => {
// const { id, created_by, update_time, ...memoryDataTemp } = detail;
res = await updateMemory({
// ...memoryDataTemp,
name: data.memory_name,
name: data.name,
id: memory?.id,
} as unknown as IMemoryAppDetailProps);
} catch (e) {
@ -268,9 +272,9 @@ export const useRenameMemory = () => {
} else {
res = await createMemory(data);
}
if (res && !memory?.id) {
navigateToMemory(res?.id)();
}
// if (res && !memory?.id) {
// navigateToMemory(res?.id)();
// }
callBack?.();
setLoading(false);
handleHideModal();

View File

@ -7,7 +7,7 @@ import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useTranslate } from '@/hooks/common-hooks';
import { pick } from 'lodash';
import { Plus } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'umi';
import { AddOrEditModal } from './add-or-edit-modal';
import { useFetchMemoryList, useRenameMemory } from './hooks';
@ -16,7 +16,8 @@ import { MemoryCard } from './memory-card';
export default function MemoryList() {
// const { data } = useFetchFlowList();
const { t } = useTranslate('memory');
const { t } = useTranslate('memories');
const [addOrEditType, setAddOrEditType] = useState<'add' | 'edit'>('add');
// const [isEdit, setIsEdit] = useState(false);
const {
data: list,
@ -43,6 +44,7 @@ export default function MemoryList() {
};
const openCreateModalFun = useCallback(() => {
// setIsEdit(false);
setAddOrEditType('add');
showMemoryRenameModal();
}, [showMemoryRenameModal]);
const handlePageChange = useCallback(
@ -121,6 +123,7 @@ export default function MemoryList() {
key={x.id}
data={x}
showMemoryRenameModal={() => {
setAddOrEditType('edit');
showMemoryRenameModal(x);
}}
></MemoryCard>
@ -152,6 +155,7 @@ export default function MemoryList() {
{openCreateModal && (
<AddOrEditModal
initialMemory={initialMemory}
isCreate={addOrEditType === 'add'}
open={openCreateModal}
loading={searchRenameLoading}
onClose={hideMemoryModal}

View File

@ -1,10 +1,3 @@
export interface ICreateMemoryProps {
memory_name: string;
memory_type: Array<string>;
embedding: string;
llm: string;
}
export interface CreateMemoryResponse {
id: string;
name: string;
@ -24,17 +17,18 @@ export type MemoryType = 'raw' | 'semantic' | 'episodic' | 'procedural';
export type StorageType = 'table' | 'graph';
export type Permissions = 'me' | 'team';
export type ForgettingPolicy = 'fifo' | 'lru';
export interface IMemory {
id: string;
export interface ICreateMemoryProps {
name: string;
memory_type: MemoryType[];
embd_id: string;
llm_id: string;
}
export interface IMemory extends ICreateMemoryProps {
id: string;
avatar: string;
tenant_id: string;
owner_name: string;
memory_type: MemoryType[];
storage_type: StorageType;
embedding: string;
llm: string;
permissions: Permissions;
description: string;
memory_size: number;

View File

@ -20,7 +20,7 @@ export function MemoryCard({ data, showMemoryRenameModal }: IProps) {
}}
moreDropdown={
<MemoryDropdown
dataset={data}
memory={data}
showMemoryRenameModal={showMemoryRenameModal}
>
<MoreButton></MoreButton>

View File

@ -12,15 +12,16 @@ import {
import { PenLine, Trash2 } from 'lucide-react';
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IMemoryAppProps, useDeleteMemory } from './hooks';
import { useDeleteMemory } from './hooks';
import { IMemory } from './interface';
export function MemoryDropdown({
children,
dataset,
memory,
showMemoryRenameModal,
}: PropsWithChildren & {
dataset: IMemoryAppProps;
showMemoryRenameModal: (dataset: IMemoryAppProps) => void;
memory: IMemory;
showMemoryRenameModal: (memory: IMemory) => void;
}) {
const { t } = useTranslation();
const { deleteMemory } = useDeleteMemory();
@ -28,13 +29,13 @@ export function MemoryDropdown({
useCallback(
(e) => {
e.stopPropagation();
showMemoryRenameModal(dataset);
showMemoryRenameModal(memory);
},
[dataset, showMemoryRenameModal],
[memory, showMemoryRenameModal],
);
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
deleteMemory({ search_id: dataset.id });
}, [dataset.id, deleteMemory]);
deleteMemory({ memory_id: memory.id });
}, [memory, deleteMemory]);
return (
<DropdownMenu>
@ -50,8 +51,9 @@ export function MemoryDropdown({
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: dataset.avatar, name: dataset.name }}
name={dataset.name}
avatar={{ avatar: memory.avatar, name: memory.name }}
name={memory.name}
warnText={t('memories.delMemoryWarn')}
/>
),
}}

View File

@ -1,3 +1,5 @@
export enum MemoryApiAction {
FetchMemoryDetail = 'fetchMemoryDetail',
FetchMemoryMessage = 'fetchMemoryMessage',
FetchMessageContent = 'fetchMessageContent',
}

View File

@ -1,59 +0,0 @@
import { useHandleSearchChange } from '@/hooks/logic-hooks';
import { getMemoryDetailById } from '@/services/memory-service';
import { useQuery } from '@tanstack/react-query';
import { useParams, useSearchParams } from 'umi';
import { MemoryApiAction } from '../constant';
import { IMessageTableProps } from '../memory-message/interface';
export const useFetchMemoryMessageList = (props?: {
refreshCount?: number;
}) => {
const { refreshCount } = props || {};
const { id } = useParams();
const [searchParams] = useSearchParams();
const memoryBaseId = searchParams.get('id') || id;
const { handleInputChange, searchString, pagination, setPagination } =
useHandleSearchChange();
let queryKey: (MemoryApiAction | number)[] = [
MemoryApiAction.FetchMemoryDetail,
];
if (typeof refreshCount === 'number') {
queryKey = [MemoryApiAction.FetchMemoryDetail, refreshCount];
}
const { data, isFetching: loading } = useQuery<IMessageTableProps>({
queryKey: [...queryKey, searchString, pagination],
initialData: {} as IMessageTableProps,
gcTime: 0,
queryFn: async () => {
if (memoryBaseId) {
const { data } = await getMemoryDetailById(memoryBaseId as string, {
// filter: {
// agent_id: '',
// },
keyword: searchString,
page: pagination.current,
page_size: pagination.pageSize,
});
// setPagination({
// page: data?.page ?? 1,
// pageSize: data?.page_size ?? 10,
// total: data?.total ?? 0,
// });
return data?.data ?? {};
} else {
return {};
}
},
});
return {
data,
loading,
handleInputChange,
searchString,
pagination,
setPagination,
};
};

View File

@ -1,14 +1,11 @@
import { useHandleSearchChange } from '@/hooks/logic-hooks';
import { IMemory } from '@/pages/memories/interface';
import { getMemoryDetailById } from '@/services/memory-service';
import memoryService from '@/services/memory-service';
import { useQuery } from '@tanstack/react-query';
import { useParams, useSearchParams } from 'umi';
import { MemoryApiAction } from '../constant';
export const useFetchMemoryBaseConfiguration = (props?: {
refreshCount?: number;
}) => {
const { refreshCount } = props || {};
export const useFetchMemoryBaseConfiguration = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const memoryBaseId = searchParams.get('id') || id;
@ -18,9 +15,6 @@ export const useFetchMemoryBaseConfiguration = (props?: {
let queryKey: (MemoryApiAction | number)[] = [
MemoryApiAction.FetchMemoryDetail,
];
if (typeof refreshCount === 'number') {
queryKey = [MemoryApiAction.FetchMemoryDetail, refreshCount];
}
const { data, isFetching: loading } = useQuery<IMemory>({
queryKey: [...queryKey, searchString, pagination],
@ -28,19 +22,9 @@ export const useFetchMemoryBaseConfiguration = (props?: {
gcTime: 0,
queryFn: async () => {
if (memoryBaseId) {
const { data } = await getMemoryDetailById(memoryBaseId as string, {
// filter: {
// agent_id: '',
// },
keyword: searchString,
page: pagination.current,
page_size: pagination.size,
});
// setPagination({
// page: data?.page ?? 1,
// pageSize: data?.page_size ?? 10,
// total: data?.total ?? 0,
// });
const { data } = await memoryService.getMemoryConfig(
memoryBaseId as string,
);
return data?.data ?? {};
} else {
return {};

View File

@ -0,0 +1,128 @@
import message from '@/components/ui/message';
import { useHandleSearchChange } from '@/hooks/logic-hooks';
import memoryService, { getMemoryDetailById } from '@/services/memory-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { useCallback, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
import { MemoryApiAction } from '../constant';
import {
IMessageContentProps,
IMessageTableProps,
} from '../memory-message/interface';
import { IMessageInfo } from './interface';
export const useFetchMemoryMessageList = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const memoryBaseId = searchParams.get('id') || id;
const { handleInputChange, searchString, pagination, setPagination } =
useHandleSearchChange();
let queryKey: (MemoryApiAction | number)[] = [
MemoryApiAction.FetchMemoryMessage,
];
const { data, isFetching: loading } = useQuery<IMessageTableProps>({
queryKey: [...queryKey, searchString, pagination],
initialData: {} as IMessageTableProps,
gcTime: 0,
queryFn: async () => {
if (memoryBaseId) {
const { data } = await getMemoryDetailById(memoryBaseId as string, {
keyword: searchString,
page: pagination.current,
page_size: pagination.pageSize,
});
return data?.data ?? {};
} else {
return {};
}
},
});
return {
data,
loading,
handleInputChange,
searchString,
pagination,
setPagination,
};
};
export const useMessageAction = () => {
const queryClient = useQueryClient();
const [selectedMessage, setSelectedMessage] = useState<IMessageInfo>(
{} as IMessageInfo,
);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const handleClickDeleteMessage = useCallback((message: IMessageInfo) => {
console.log('handleClickDeleteMessage', message);
setSelectedMessage(message);
setShowDeleteDialog(true);
}, []);
const handleDeleteMessage = useCallback(() => {
// delete message
memoryService.deleteMemoryMessage(selectedMessage.message_id).then(() => {
message.success(t('message.deleted'));
queryClient.invalidateQueries({
queryKey: [MemoryApiAction.FetchMemoryMessage],
});
});
setShowDeleteDialog(false);
}, [selectedMessage.message_id, queryClient]);
const [showMessageContentDialog, setShowMessageContentDialog] =
useState(false);
const [selectedMessageContent, setSelectedMessageContent] =
useState<IMessageContentProps>({} as IMessageContentProps);
const {
data: messageContent,
isPending: fetchMessageContentLoading,
mutateAsync: fetchMessageContent,
} = useMutation<IMessageContentProps>({
mutationKey: [
MemoryApiAction.FetchMessageContent,
selectedMessage.message_id,
],
mutationFn: async () => {
setShowMessageContentDialog(true);
const res = await memoryService.getMessageContent(
selectedMessage.message_id,
);
if (res.data.code === 0) {
setSelectedMessageContent(res.data.data);
} else {
message.error(res.data.message);
}
return res.data.data;
},
});
const handleClickMessageContentDialog = useCallback(
(message: IMessageInfo) => {
setSelectedMessage(message);
fetchMessageContent();
},
[fetchMessageContent],
);
return {
selectedMessage,
setSelectedMessage,
showDeleteDialog,
setShowDeleteDialog,
handleClickDeleteMessage,
handleDeleteMessage,
messageContent,
fetchMessageContentLoading,
fetchMessageContent,
selectedMessageContent,
showMessageContentDialog,
setShowMessageContentDialog,
handleClickMessageContentDialog,
};
};

View File

@ -1,6 +1,6 @@
import ListFilterBar from '@/components/list-filter-bar';
import { t } from 'i18next';
import { useFetchMemoryMessageList } from '../hooks/use-memory-messages';
import { useFetchMemoryMessageList } from './hook';
import { MemoryTable } from './message-table';
export default function MemoryMessage() {

View File

@ -17,3 +17,8 @@ export interface IMessageTableProps {
messages: { message_list: Array<IMessageInfo>; total: number };
storage_type: string;
}
export interface IMessageContentProps {
content: string;
content_embed: string;
}

View File

@ -1,3 +1,23 @@
import {
ConfirmDeleteDialog,
ConfirmDeleteDialogNode,
} from '@/components/confirm-delete-dialog';
import { EmptyType } from '@/components/empty/constant';
import Empty from '@/components/empty/empty';
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Switch } from '@/components/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Pagination } from '@/interfaces/common';
import { replaceText } from '@/pages/dataset/process-log-modal';
import {
ColumnDef,
ColumnFiltersState,
@ -10,26 +30,13 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import * as React from 'react';
import { EmptyType } from '@/components/empty/constant';
import Empty from '@/components/empty/empty';
import { Button } from '@/components/ui/button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { Switch } from '@/components/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Pagination } from '@/interfaces/common';
import { t } from 'i18next';
import { pick } from 'lodash';
import { Eraser, TextSelect } from 'lucide-react';
import { useMemo } from 'react';
import { Copy, Eraser, TextSelect } from 'lucide-react';
import * as React from 'react';
import { useMemo, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { useMessageAction } from './hook';
import { IMessageInfo } from './interface';
export type MemoryTableProps = {
@ -51,13 +58,27 @@ export function MemoryTable({
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [copied, setCopied] = useState(false);
const {
showDeleteDialog,
setShowDeleteDialog,
handleClickDeleteMessage,
selectedMessage,
handleDeleteMessage,
fetchMessageContent,
selectedMessageContent,
showMessageContentDialog,
setShowMessageContentDialog,
handleClickMessageContentDialog,
} = useMessageAction();
// Define columns for the memory table
const columns: ColumnDef<IMessageInfo>[] = useMemo(
() => [
{
accessorKey: 'session_id',
header: () => <span>{t('memoryDetail.messages.sessionId')}</span>,
header: () => <span>{t('memory.messages.sessionId')}</span>,
cell: ({ row }) => (
<div className="text-sm font-medium ">
{row.getValue('session_id')}
@ -66,7 +87,7 @@ export function MemoryTable({
},
{
accessorKey: 'agent_name',
header: () => <span>{t('memoryDetail.messages.agent')}</span>,
header: () => <span>{t('memory.messages.agent')}</span>,
cell: ({ row }) => (
<div className="text-sm font-medium ">
{row.getValue('agent_name')}
@ -75,7 +96,7 @@ export function MemoryTable({
},
{
accessorKey: 'message_type',
header: () => <span>{t('memoryDetail.messages.type')}</span>,
header: () => <span>{t('memory.messages.type')}</span>,
cell: ({ row }) => (
<div className="text-sm font-medium capitalize">
{row.getValue('message_type')}
@ -84,28 +105,28 @@ export function MemoryTable({
},
{
accessorKey: 'valid_at',
header: () => <span>{t('memoryDetail.messages.validDate')}</span>,
header: () => <span>{t('memory.messages.validDate')}</span>,
cell: ({ row }) => (
<div className="text-sm ">{row.getValue('valid_at')}</div>
),
},
{
accessorKey: 'forget_at',
header: () => <span>{t('memoryDetail.messages.forgetAt')}</span>,
header: () => <span>{t('memory.messages.forgetAt')}</span>,
cell: ({ row }) => (
<div className="text-sm ">{row.getValue('forget_at')}</div>
),
},
{
accessorKey: 'source_id',
header: () => <span>{t('memoryDetail.messages.source')}</span>,
header: () => <span>{t('memory.messages.source')}</span>,
cell: ({ row }) => (
<div className="text-sm ">{row.getValue('source_id')}</div>
),
},
{
accessorKey: 'status',
header: () => <span>{t('memoryDetail.messages.enable')}</span>,
header: () => <span>{t('memory.messages.enable')}</span>,
cell: ({ row }) => {
const isEnabled = row.getValue('status') as boolean;
return (
@ -117,19 +138,28 @@ export function MemoryTable({
},
{
accessorKey: 'action',
header: () => <span>{t('memoryDetail.messages.action')}</span>,
header: () => <span>{t('memory.messages.action')}</span>,
meta: {
cellClassName: 'w-12',
},
cell: () => (
cell: ({ row }) => (
<div className=" flex opacity-0 group-hover:opacity-100">
<Button variant={'ghost'} className="bg-transparent">
<Button
variant={'ghost'}
className="bg-transparent"
onClick={() => {
handleClickMessageContentDialog(row.original);
}}
>
<TextSelect />
</Button>
<Button
variant={'delete'}
className="bg-transparent"
aria-label="Edit"
onClick={() => {
handleClickDeleteMessage(row.original);
}}
>
<Eraser />
</Button>
@ -137,7 +167,7 @@ export function MemoryTable({
),
},
],
[],
[handleClickDeleteMessage],
);
const currentPagination = useMemo(() => {
@ -210,6 +240,85 @@ export function MemoryTable({
)}
</TableBody>
</Table>
{showDeleteDialog && (
<ConfirmDeleteDialog
onOk={handleDeleteMessage}
title={t('memory.messages.forgetMessage')}
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
content={{
node: (
<ConfirmDeleteDialogNode
// avatar={{ avatar: selectedMessage.avatar, name: selectedMessage.name }}
name={
t('memory.messages.sessionId') +
': ' +
selectedMessage.session_id
}
warnText={t('memory.messages.delMessageWarn')}
/>
),
}}
/>
)}
{showMessageContentDialog && (
<Modal
title={t('memory.messages.content')}
open={showMessageContentDialog}
onOpenChange={setShowMessageContentDialog}
className="!w-[640px]"
footer={
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setShowMessageContentDialog(false)}
className={
'px-2 py-1 border border-border-button rounded-md hover:bg-bg-card hover:text-text-primary '
}
>
{t('common.close')}
</button>
</div>
}
>
<div className="flex flex-col gap-2.5">
<div className="text-text-secondary text-sm">
{t('memory.messages.sessionId')}:&nbsp;&nbsp;
{selectedMessage.session_id}
</div>
{selectedMessageContent?.content && (
<div className="w-full bg-accent-primary-5 whitespace-pre-line text-wrap rounded-lg h-fit max-h-[350px] overflow-y-auto scrollbar-auto px-2.5 py-1">
{replaceText(selectedMessageContent?.content || '')}
</div>
)}
{selectedMessageContent?.content_embed && (
<div className="flex gap-2 items-center">
<CopyToClipboard
text={selectedMessageContent?.content_embed}
onCopy={() => {
setCopied(true);
setTimeout(() => setCopied(false), 1000);
}}
>
<Button
variant={'ghost'}
className="border border-border-button "
>
{t('memory.messages.contentEmbed')}
<Copy />
</Button>
</CopyToClipboard>
{copied && (
<span className="text-xs text-text-secondary">
{t('memory.messages.copied')}
</span>
)}
</div>
)}
</div>
</Modal>
)}
<div className="flex items-center justify-end py-4 absolute bottom-3 right-3">
<RAGFlowPagination

View File

@ -0,0 +1,159 @@
import { FormFieldType, RenderField } from '@/components/dynamic-form';
import { SingleFormSlider } from '@/components/ui/dual-range-slider';
import { NumberInput } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { ListChevronsDownUp, ListChevronsUpDown } from 'lucide-react';
import { useState } from 'react';
import { z } from 'zod';
export const advancedSettingsFormSchema = {
permission: z.string().optional(),
storage_type: z.enum(['table', 'graph']).optional(),
forget_policy: z.enum(['lru', 'fifo']).optional(),
temperature: z.number().optional(),
system_prompt: z.string().optional(),
user_prompt: z.string().optional(),
};
export const defaultAdvancedSettingsForm = {
permission: 'me',
storage_type: 'table',
forget_policy: 'fifo',
temperature: 0.7,
system_prompt: '',
user_prompt: '',
};
export const AdvancedSettingsForm = () => {
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
return (
<>
<div
className="flex items-center gap-1 w-full cursor-pointer"
onClick={() => setShowAdvancedSettings(!showAdvancedSettings)}
>
{showAdvancedSettings ? (
<ListChevronsDownUp size={14} />
) : (
<ListChevronsUpDown size={14} />
)}
{t('memory.config.advancedSettings')}
</div>
{/* {showAdvancedSettings && ( */}
<>
<RenderField
field={{
name: 'permission',
label: t('memory.config.permission'),
required: false,
horizontal: true,
// hideLabel: true,
type: FormFieldType.Custom,
render: (field) => (
<RadioGroup
defaultValue="me"
className="flex"
{...field}
onValueChange={(value) => {
console.log(value);
field.onChange(value);
}}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="me" id="r1" />
<Label htmlFor="r1">{t('memory.config.onlyMe')}</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="team" id="r2" />
<Label htmlFor="r2">{t('memory.config.team')}</Label>
</div>
</RadioGroup>
),
}}
/>
<RenderField
field={{
name: 'storage_type',
label: t('memory.config.storageType'),
type: FormFieldType.Select,
horizontal: true,
placeholder: t('memory.config.storageTypePlaceholder'),
options: [
{ label: 'table', value: 'table' },
// { label: 'graph', value: 'graph' },
],
required: false,
}}
/>
<RenderField
field={{
name: 'forget_policy',
label: t('memory.config.forgetPolicy'),
type: FormFieldType.Select,
horizontal: true,
// placeholder: t('memory.config.storageTypePlaceholder'),
options: [
{ label: 'lru', value: 'lru' },
{ label: 'fifo', value: 'fifo' },
],
required: false,
}}
/>
<RenderField
field={{
name: 'temperature',
label: t('memory.config.temperature'),
type: FormFieldType.Custom,
horizontal: true,
required: false,
render: (field) => (
<div className="flex gap-2 items-center">
<SingleFormSlider
{...field}
onChange={(value: number) => {
field.onChange(value);
}}
max={1}
step={0.01}
min={0}
disabled={false}
></SingleFormSlider>
<NumberInput
className={cn(
'h-6 w-10 p-1 border border-border-button rounded-sm',
)}
max={1}
step={0.01}
min={0}
{...field}
></NumberInput>
</div>
),
}}
/>
<RenderField
field={{
name: 'system_prompt',
label: t('memory.config.systemPrompt'),
type: FormFieldType.Textarea,
horizontal: true,
placeholder: t('memory.config.systemPromptPlaceholder'),
required: false,
}}
/>
<RenderField
field={{
name: 'user_prompt',
label: t('memory.config.userPrompt'),
type: FormFieldType.Text,
horizontal: true,
placeholder: t('memory.config.userPromptPlaceholder'),
required: false,
}}
/>
</>
{/* )} */}
</>
);
};

View File

@ -0,0 +1,53 @@
import { AvatarUpload } from '@/components/avatar-upload';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { t } from 'i18next';
import { z } from 'zod';
export const basicInfoSchema = {
name: z.string().min(1, { message: t('setting.nameRequired') }),
avatar: z.string().optional(),
description: z.string().optional(),
};
export const defaultBasicInfo = { name: '', avatar: '', description: '' };
export const BasicInfo = () => {
return (
<>
<RAGFlowFormItem
name={'name'}
label={t('memories.name')}
required={true}
horizontal={true}
// tooltip={field.tooltip}
// labelClassName={labelClassName || field.labelClassName}
>
{(field) => {
return <Input {...field}></Input>;
}}
</RAGFlowFormItem>
<RAGFlowFormItem
name={'avatar'}
label={t('memory.config.avatar')}
required={false}
horizontal={true}
// tooltip={field.tooltip}
// labelClassName={labelClassName || field.labelClassName}
>
{(field) => {
return <AvatarUpload {...field}></AvatarUpload>;
}}
</RAGFlowFormItem>
<RAGFlowFormItem
name={'description'}
label={t('memory.config.description')}
required={false}
horizontal={true}
// tooltip={field.tooltip}
// labelClassName={labelClassName || field.labelClassName}
>
{(field) => {
return <Input {...field}></Input>;
}}
</RAGFlowFormItem>
</>
);
};

View File

@ -0,0 +1,42 @@
import { useUpdateMemory } from '@/pages/memories/hooks';
import { IMemory, IMemoryAppDetailProps } from '@/pages/memories/interface';
import { omit } from 'lodash';
import { useCallback, useState } from 'react';
export const useUpdateMemoryConfig = () => {
const { updateMemory } = useUpdateMemory();
const [loading, setLoading] = useState(false);
const onMemoryRenameOk = useCallback(
async (data: IMemory) => {
let res;
setLoading(true);
if (data?.id) {
// console.log('memory-->', memory, data);
try {
const params = omit(data, [
'id',
'memory_type',
'embd_id',
'storage_type',
]);
res = await updateMemory({
// ...memoryDataTemp,
// data: data,
id: data.id,
...params,
} as unknown as IMemoryAppDetailProps);
// if (res && res.data.code === 0) {
// message.success(t('message.update_success'));
// } else {
// message.error(t('message.update_fail'));
// }
} catch (e) {
console.error('error', e);
}
}
setLoading(false);
},
[updateMemory],
);
return { onMemoryRenameOk, loading };
};

View File

@ -1,13 +1,110 @@
import { DynamicForm } from '@/components/dynamic-form';
import { Button } from '@/components/ui/button';
import Divider from '@/components/ui/divider';
import { Form } from '@/components/ui/form';
import { MainContainer } from '@/pages/dataset/dataset-setting/configuration-form-container';
import { TopTitle } from '@/pages/dataset/dataset-title';
import { IMemory } from '@/pages/memories/interface';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useFetchMemoryBaseConfiguration } from '../hooks/use-memory-setting';
import {
AdvancedSettingsForm,
advancedSettingsFormSchema,
defaultAdvancedSettingsForm,
} from './advanced-settings-form';
import { BasicInfo, basicInfoSchema, defaultBasicInfo } from './basic-form';
import { useUpdateMemoryConfig } from './hook';
import {
MemoryModelForm,
defaultMemoryModelForm,
memoryModelFormSchema,
} from './memory-model-form';
const MemoryMessageSchema = z.object({
id: z.string(),
...basicInfoSchema,
...memoryModelFormSchema,
...advancedSettingsFormSchema,
});
// type MemoryMessageForm = z.infer<typeof MemoryMessageSchema>;
export default function MemoryMessage() {
const form = useForm<IMemory>({
resolver: zodResolver(MemoryMessageSchema),
defaultValues: {
id: '',
...defaultBasicInfo,
...defaultMemoryModelForm,
...defaultAdvancedSettingsForm,
} as unknown as IMemory,
});
const { data } = useFetchMemoryBaseConfiguration();
const { onMemoryRenameOk, loading } = useUpdateMemoryConfig();
useEffect(() => {
form.reset({
id: data?.id,
embd_id: data?.embd_id,
llm_id: data?.llm_id,
name: data?.name || '',
description: data?.description || '',
avatar: data?.avatar || '',
memory_size: data?.memory_size,
memory_type: data?.memory_type,
temperature: data?.temperature,
system_prompt: data?.system_prompt || '',
user_prompt: data?.user_prompt || '',
forgetting_policy: data?.forgetting_policy || 'fifo',
permissions: data?.permissions || 'me',
});
}, [data, form]);
const onSubmit = (data: IMemory) => {
onMemoryRenameOk(data);
};
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full bg-text-secondary">11</div>
<div className="h-4 w-4 rounded-full bg-text-secondary">11</div>
<section className="h-full flex flex-col">
<TopTitle
title={t('knowledgeDetails.configuration')}
description={t('knowledgeConfiguration.titleDescription')}
></TopTitle>
<div className="flex gap-14 flex-1 min-h-0">
<Form {...form}>
<form onSubmit={form.handleSubmit(() => {})} className="space-y-6 ">
<div className="w-[768px] h-[calc(100vh-300px)] pr-1 overflow-y-auto scrollbar-auto">
<MainContainer className="text-text-secondary !space-y-10">
<div className="text-base font-medium text-text-primary">
{t('knowledgeConfiguration.baseInfo')}
</div>
<BasicInfo></BasicInfo>
<Divider />
<MemoryModelForm />
<AdvancedSettingsForm />
</MainContainer>
</div>
<div className="text-right items-center flex justify-end gap-3 w-[768px]">
<Button
type="reset"
className="bg-transparent text-color-white hover:bg-transparent border-border-button border"
onClick={() => {
form.reset();
}}
>
{t('knowledgeConfiguration.cancel')}
</Button>
<DynamicForm.SavingButton
submitLoading={loading}
submitFunc={(value) => {
console.log('form-value', value);
onSubmit(value as IMemory);
}}
></DynamicForm.SavingButton>
</div>
</form>
</Form>
</div>
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full bg-text ">setting</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,74 @@
import { FormFieldType, RenderField } from '@/components/dynamic-form';
import { useModelOptions } from '@/components/llm-setting-items/llm-form-field';
import { EmbeddingSelect } from '@/pages/dataset/dataset-setting/configuration/common-item';
import { t } from 'i18next';
import { z } from 'zod';
export const memoryModelFormSchema = {
embd_id: z.string(),
llm_id: z.string(),
memory_type: z.array(z.string()).optional(),
memory_size: z.number().optional(),
};
export const defaultMemoryModelForm = {
embd_id: '',
llm_id: '',
memory_type: [],
memory_size: 0,
};
export const MemoryModelForm = () => {
const { modelOptions } = useModelOptions();
return (
<>
<RenderField
field={{
name: 'embd_id',
label: t('memories.embeddingModel'),
placeholder: t('memories.selectModel'),
required: true,
horizontal: true,
// hideLabel: true,
type: FormFieldType.Custom,
render: (field) => <EmbeddingSelect field={field} isEdit={false} />,
}}
/>
<RenderField
field={{
name: 'llm_id',
label: t('memories.llm'),
placeholder: t('memories.selectModel'),
required: true,
horizontal: true,
type: FormFieldType.Select,
options: modelOptions as { value: string; label: string }[],
}}
/>
<RenderField
field={{
name: 'memory_type',
label: t('memories.memoryType'),
type: FormFieldType.MultiSelect,
horizontal: true,
placeholder: t('memories.memoryTypePlaceholder'),
options: [
{ label: 'Raw', value: 'raw' },
{ label: 'Semantic', value: 'semantic' },
{ label: 'Episodic', value: 'episodic' },
{ label: 'Procedural', value: 'procedural' },
],
required: true,
}}
/>
<RenderField
field={{
name: 'memory_size',
label: t('memory.config.memorySize'),
type: FormFieldType.Number,
horizontal: true,
// placeholder: t('memory.config.memorySizePlaceholder'),
required: false,
}}
/>
</>
);
};

View File

@ -1,36 +1,31 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { useSecondPathName } from '@/hooks/route-hook';
import { cn, formatBytes } from '@/lib/utils';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date';
import { Banknote, Logs } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useFetchMemoryBaseConfiguration } from '../hooks/use-memory-setting';
import { useHandleMenuClick } from './hooks';
type PropType = {
refreshCount?: number;
};
export function SideBar({ refreshCount }: PropType) {
export function SideBar() {
const pathName = useSecondPathName();
const { handleMenuClick } = useHandleMenuClick();
// refreshCount: be for avatar img sync update on top left
const { data } = useFetchMemoryBaseConfiguration({ refreshCount });
const { data } = useFetchMemoryBaseConfiguration();
const { t } = useTranslation();
const items = useMemo(() => {
const list = [
{
icon: <Logs className="size-4" />,
label: t(`knowledgeDetails.overview`),
label: t(`memory.sideBar.messages`),
key: Routes.MemoryMessage,
},
{
icon: <Banknote className="size-4" />,
label: t(`knowledgeDetails.configuration`),
label: t(`memory.sideBar.configuration`),
key: Routes.MemorySetting,
},
];
@ -49,15 +44,15 @@ export function SideBar({ refreshCount }: PropType) {
<h3 className="text-lg font-semibold line-clamp-1 text-text-primary text-ellipsis overflow-hidden">
{data.name}
</h3>
<div className="flex justify-between">
{/* <div className="flex justify-between">
<span>
{data.doc_num} {t('knowledgeDetails.files')}
</span>
<span>{formatBytes(data.size)}</span>
</div>
<div>
{t('knowledgeDetails.created')} {formatPureDate(data.create_time)}
</div>
{t('knowledgeDetails.created')} {formatPureDate(data.)}
</div> */}
</div>
</div>

View File

@ -8,6 +8,9 @@ const {
deleteMemory,
getMemoryDetail,
updateMemorySetting,
getMemoryConfig,
deleteMemoryMessage,
getMessageContent,
// getMemoryDetailShare,
} = api;
const methods = {
@ -17,27 +20,21 @@ const methods = {
},
getMemoryList: {
url: getMemoryList,
method: 'post',
method: 'get',
},
deleteMemory: { url: deleteMemory, method: 'post' },
// getMemoryDetail: {
// url: getMemoryDetail,
// method: 'get',
// },
// updateMemorySetting: {
// url: updateMemorySetting,
// method: 'post',
// },
// getMemoryDetailShare: {
// url: getMemoryDetailShare,
// method: 'get',
// },
deleteMemory: { url: deleteMemory, method: 'delete' },
getMemoryConfig: {
url: getMemoryConfig,
method: 'get',
},
deleteMemoryMessage: { url: deleteMemoryMessage, method: 'delete' },
getMessageContent: { url: getMessageContent, method: 'get' },
} as const;
const memoryService = registerNextServer<keyof typeof methods>(methods);
export const updateMemoryById = (id: string, data: any) => {
return request.post(updateMemorySetting(id), { data });
return request.put(updateMemorySetting(id), { data });
};
export const getMemoryDetailById = (id: string, data: any) => {
return request.post(getMemoryDetail(id), { data });
return request.get(getMemoryDetail(id), { params: data });
};
export default memoryService;

View File

@ -227,11 +227,15 @@ export default {
retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`,
// memory
createMemory: `${api_host}/memory/create`,
getMemoryList: `${api_host}/memory/list`,
createMemory: `${api_host}/memories`,
getMemoryList: `${api_host}/memories`,
getMemoryConfig: (id: string) => `${api_host}/memories/${id}/config`,
deleteMemory: (id: string) => `${api_host}/memory/rm/${id}`,
getMemoryDetail: (id: string) => `${api_host}/memory/detail/${id}`,
updateMemorySetting: (id: string) => `${api_host}/memory/update/${id}`,
getMemoryDetail: (id: string) => `${api_host}/memories/${id}`,
updateMemorySetting: (id: string) => `${api_host}/memories/${id}`,
deleteMemoryMessage: (id: string) => `${api_host}/message/rm/${id}`,
getMessageContent: (message_id: string) =>
`${api_host}/messages/${message_id}/content`,
// data pipeline
fetchDataflow: (id: string) => `${api_host}/dataflow/get/${id}`,