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

@ -0,0 +1,75 @@
import { DynamicForm, DynamicFormRef } from '@/components/dynamic-form';
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 { createMemoryFields } from './constants';
import { IMemory } from './interface';
type IProps = {
open: boolean;
onClose: () => void;
onSubmit?: (data: any) => void;
initialMemory: IMemory;
loading?: boolean;
};
export const AddOrEditModal = (props: IProps) => {
const { open, onClose, onSubmit, initialMemory } = props;
// const [fields, setFields] = useState<FormFieldConfig[]>(createMemoryFields);
// const formRef = useRef<DynamicFormRef>(null);
const [formInstance, setFormInstance] = useState<DynamicFormRef | null>(null);
const formCallbackRef = useCallback((node: DynamicFormRef | null) => {
if (node) {
// formRef.current = node;
setFormInstance(node);
}
}, []);
const { modelOptions } = useModelOptions();
useEffect(() => {
if (initialMemory && initialMemory.id) {
formInstance?.onFieldUpdate('memory_type', { hidden: true });
formInstance?.onFieldUpdate('embedding', { hidden: true });
formInstance?.onFieldUpdate('llm', { hidden: true });
} else {
formInstance?.onFieldUpdate('llm', { options: modelOptions as any });
}
}, [modelOptions, formInstance, initialMemory]);
return (
<Modal
open={open}
onOpenChange={onClose}
className="!w-[480px]"
title={
<div className="flex flex-col">
<div>
<HomeIcon name="memory" width={'24'} />
</div>
{t('memory.createMemory')}
</div>
}
showfooter={false}
confirmLoading={props.loading}
>
<DynamicForm.Root
ref={formCallbackRef}
fields={createMemoryFields}
onSubmit={() => {}}
defaultValues={initialMemory}
>
<div className="flex justify-end gap-2 pb-5">
<DynamicForm.CancelButton handleCancel={onClose} />
<DynamicForm.SavingButton
submitLoading={false}
submitFunc={(data) => {
onSubmit?.(data);
}}
/>
</div>
</DynamicForm.Root>
</Modal>
);
};

View File

@ -0,0 +1,41 @@
import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form';
import { EmbeddingSelect } from '@/pages/dataset/dataset-setting/configuration/common-item';
import { t } from 'i18next';
export const createMemoryFields = [
{
name: 'memory_name',
label: t('memory.name'),
placeholder: t('memory.memoryNamePlaceholder'),
required: true,
},
{
name: 'memory_type',
label: t('memory.memoryType'),
type: FormFieldType.MultiSelect,
placeholder: t('memory.descriptionPlaceholder'),
options: [
{ label: 'Raw', value: 'raw' },
{ label: 'Semantic', value: 'semantic' },
{ label: 'Episodic', value: 'episodic' },
{ label: 'Procedural', value: 'procedural' },
],
required: true,
},
{
name: 'embedding',
label: t('memory.embeddingModel'),
placeholder: t('memory.selectModel'),
required: true,
// hideLabel: true,
// type: 'custom',
render: (field) => <EmbeddingSelect field={field} isEdit={false} />,
},
{
name: 'llm',
label: t('memory.llm'),
placeholder: t('memory.selectModel'),
required: true,
type: FormFieldType.Select,
},
] as FormFieldConfig[];

View File

@ -0,0 +1,288 @@
// src/pages/next-memoryes/hooks.ts
import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks';
import { useHandleSearchChange } from '@/hooks/logic-hooks';
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 { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi';
import {
CreateMemoryResponse,
DeleteMemoryProps,
DeleteMemoryResponse,
ICreateMemoryProps,
IMemory,
IMemoryAppDetailProps,
MemoryDetailResponse,
MemoryListResponse,
} from './interface';
export const useCreateMemory = () => {
const { t } = useTranslation();
const {
data,
isError,
mutateAsync: createMemoryMutation,
} = useMutation<CreateMemoryResponse, Error, ICreateMemoryProps>({
mutationKey: ['createMemory'],
mutationFn: async (props) => {
const { data: response } = await memoryService.createMemory(props);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to create memory');
}
return response.data;
},
onSuccess: () => {
message.success(t('message.created'));
},
onError: (error) => {
message.error(t('message.error', { error: error.message }));
},
});
const createMemory = useCallback(
(props: ICreateMemoryProps) => {
return createMemoryMutation(props);
},
[createMemoryMutation],
);
return { data, isError, createMemory };
};
export const useFetchMemoryList = () => {
const { handleInputChange, searchString, pagination, setPagination } =
useHandleSearchChange();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });
const { data, isLoading, isError, refetch } = useQuery<
MemoryListResponse,
Error
>({
queryKey: [
'memoryList',
{
debouncedSearchString,
...pagination,
},
],
queryFn: async () => {
const { data: response } = await memoryService.getMemoryList(
{
params: {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
data: {},
},
true,
);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to fetch memory list');
}
console.log(response);
return response;
},
});
// const setMemoryListParams = (newParams: MemoryListParams) => {
// setMemoryParams((prevParams) => ({
// ...prevParams,
// ...newParams,
// }));
// };
return {
data,
isLoading,
isError,
pagination,
searchString,
handleInputChange,
setPagination,
refetch,
};
};
export const useFetchMemoryDetail = (tenantId?: string) => {
const { id } = useParams();
const [memoryParams] = useSearchParams();
const shared_id = memoryParams.get('shared_id');
const memoryId = id || shared_id;
let param: { id: string | null; tenant_id?: string } = {
id: memoryId,
};
if (shared_id) {
param = {
id: memoryId,
tenant_id: tenantId,
};
}
const fetchMemoryDetailFunc = shared_id
? memoryService.getMemoryDetailShare
: memoryService.getMemoryDetail;
const { data, isLoading, isError } = useQuery<MemoryDetailResponse, Error>({
queryKey: ['memoryDetail', memoryId],
enabled: !shared_id || !!tenantId,
queryFn: async () => {
const { data: response } = await fetchMemoryDetailFunc(param);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to fetch memory detail');
}
return response;
},
});
return { data: data?.data, isLoading, isError };
};
export const useDeleteMemory = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isError,
mutateAsync: deleteMemoryMutation,
} = useMutation<DeleteMemoryResponse, Error, DeleteMemoryProps>({
mutationKey: ['deleteMemory'],
mutationFn: async (props) => {
const { data: response } = await memoryService.deleteMemory(props);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to delete memory');
}
queryClient.invalidateQueries({ queryKey: ['memoryList'] });
return response;
},
onSuccess: () => {
message.success(t('message.deleted'));
},
onError: (error) => {
message.error(t('message.error', { error: error.message }));
},
});
const deleteMemory = useCallback(
(props: DeleteMemoryProps) => {
return deleteMemoryMutation(props);
},
[deleteMemoryMutation],
);
return { data, isError, deleteMemory };
};
export const useUpdateMemory = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isError,
mutateAsync: updateMemoryMutation,
} = useMutation<any, Error, IMemoryAppDetailProps>({
mutationKey: ['updateMemory'],
mutationFn: async (formData) => {
const { data: response } = await updateMemoryById(formData.id, formData);
if (response.code !== 0) {
throw new Error(response.message || 'Failed to update memory');
}
return response.data;
},
onSuccess: (data, variables) => {
message.success(t('message.updated'));
queryClient.invalidateQueries({
queryKey: ['memoryDetail', variables.id],
});
},
onError: (error) => {
message.error(t('message.error', { error: error.message }));
},
});
const updateMemory = useCallback(
(formData: IMemoryAppDetailProps) => {
return updateMemoryMutation(formData);
},
[updateMemoryMutation],
);
return { data, isError, updateMemory };
};
export const useRenameMemory = () => {
const [memory, setMemory] = useState<IMemory>({} as IMemory);
const { navigateToMemory } = useNavigatePage();
const {
visible: openCreateModal,
hideModal: hideChatRenameModal,
showModal: showChatRenameModal,
} = useSetModalState();
const { updateMemory } = useUpdateMemory();
const { createMemory } = useCreateMemory();
const [loading, setLoading] = useState(false);
const handleShowChatRenameModal = useCallback(
(record?: IMemory) => {
if (record) {
setMemory(record);
}
showChatRenameModal();
},
[showChatRenameModal],
);
const handleHideModal = useCallback(() => {
hideChatRenameModal();
setMemory({} as IMemory);
}, [hideChatRenameModal]);
const onMemoryRenameOk = useCallback(
async (data: ICreateMemoryProps, callBack?: () => void) => {
let res;
setLoading(true);
if (memory?.id) {
try {
// const reponse = await memoryService.getMemoryDetail({
// id: memory?.id,
// });
// const detail = reponse.data?.data;
// console.log('detail-->', detail);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// const { id, created_by, update_time, ...memoryDataTemp } = detail;
res = await updateMemory({
// ...memoryDataTemp,
name: data.memory_name,
id: memory?.id,
} as unknown as IMemoryAppDetailProps);
} catch (e) {
console.error('error', e);
}
} else {
res = await createMemory(data);
}
if (res && !memory?.id) {
navigateToMemory(res?.id)();
}
callBack?.();
setLoading(false);
handleHideModal();
},
[memory, createMemory, handleHideModal, navigateToMemory, updateMemory],
);
return {
memoryRenameLoading: loading,
initialMemory: memory,
onMemoryRenameOk,
openCreateModal,
hideMemoryModal: handleHideModal,
showMemoryRenameModal: handleShowChatRenameModal,
};
};

View File

@ -0,0 +1,163 @@
import { CardContainer } from '@/components/card-container';
import { EmptyCardType } from '@/components/empty/constant';
import { EmptyAppCard } from '@/components/empty/empty';
import ListFilterBar from '@/components/list-filter-bar';
import { Button } from '@/components/ui/button';
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 { useSearchParams } from 'umi';
import { AddOrEditModal } from './add-or-edit-modal';
import { useFetchMemoryList, useRenameMemory } from './hooks';
import { ICreateMemoryProps } from './interface';
import { MemoryCard } from './memory-card';
export default function MemoryList() {
// const { data } = useFetchFlowList();
const { t } = useTranslate('memory');
// const [isEdit, setIsEdit] = useState(false);
const {
data: list,
pagination,
searchString,
handleInputChange,
setPagination,
refetch: refetchList,
} = useFetchMemoryList();
const {
openCreateModal,
showMemoryRenameModal,
hideMemoryModal,
searchRenameLoading,
onMemoryRenameOk,
initialMemory,
} = useRenameMemory();
const onMemoryConfirm = (data: ICreateMemoryProps) => {
onMemoryRenameOk(data, () => {
refetchList();
});
};
const openCreateModalFun = useCallback(() => {
// setIsEdit(false);
showMemoryRenameModal();
}, [showMemoryRenameModal]);
const handlePageChange = useCallback(
(page: number, pageSize?: number) => {
setPagination({ page, pageSize });
},
[setPagination],
);
const [searchUrl, setMemoryUrl] = useSearchParams();
const isCreate = searchUrl.get('isCreate') === 'true';
useEffect(() => {
if (isCreate) {
openCreateModalFun();
searchUrl.delete('isCreate');
setMemoryUrl(searchUrl);
}
}, [isCreate, openCreateModalFun, searchUrl, setMemoryUrl]);
return (
<section className="w-full h-full flex flex-col">
{(!list?.data?.memory_list?.length ||
list?.data?.memory_list?.length <= 0) &&
!searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
{(!!list?.data?.memory_list?.length || searchString) && (
<>
<div className="px-8 pt-8">
<ListFilterBar
icon="memory"
title={t('memory')}
showFilter={false}
onSearchChange={handleInputChange}
searchString={searchString}
>
<Button
variant={'default'}
onClick={() => {
openCreateModalFun();
}}
>
<Plus className="mr-2 h-4 w-4" />
{t('createMemory')}
</Button>
</ListFilterBar>
</div>
{(!list?.data?.memory_list?.length ||
list?.data?.memory_list?.length <= 0) &&
searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
<div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{list?.data.memory_list.map((x) => {
return (
<MemoryCard
key={x.id}
data={x}
showMemoryRenameModal={() => {
showMemoryRenameModal(x);
}}
></MemoryCard>
);
})}
</CardContainer>
</div>
{list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
// total={pagination.total}
total={list?.data.total}
onChange={handlePageChange}
/>
</div>
)}
</>
)}
{/* {openCreateModal && (
<RenameDialog
hideModal={hideMemoryRenameModal}
onOk={onMemoryRenameConfirm}
initialName={initialMemoryName}
loading={searchRenameLoading}
title={<HomeIcon name="memory" width={'24'} />}
></RenameDialog>
)} */}
{openCreateModal && (
<AddOrEditModal
initialMemory={initialMemory}
open={openCreateModal}
loading={searchRenameLoading}
onClose={hideMemoryModal}
onSubmit={onMemoryConfirm}
/>
)}
</section>
);
}

View File

@ -0,0 +1,121 @@
export interface ICreateMemoryProps {
memory_name: string;
memory_type: Array<string>;
embedding: string;
llm: string;
}
export interface CreateMemoryResponse {
id: string;
name: string;
description: string;
}
export interface MemoryListParams {
keywords?: string;
parser_id?: string;
page?: number;
page_size?: number;
orderby?: string;
desc?: boolean;
owner_ids?: string;
}
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;
name: 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;
forgetting_policy: ForgettingPolicy;
temperature: string;
system_prompt: string;
user_prompt: string;
}
export interface MemoryListResponse {
code: number;
data: {
memory_list: Array<IMemory>;
total: number;
};
message: string;
}
export interface DeleteMemoryProps {
memory_id: string;
}
export interface DeleteMemoryResponse {
code: number;
data: boolean;
message: string;
}
export interface IllmSettingProps {
llm_id: string;
parameter: string;
temperature?: number;
top_p?: number;
frequency_penalty?: number;
presence_penalty?: number;
}
interface IllmSettingEnableProps {
temperatureEnabled?: boolean;
topPEnabled?: boolean;
presencePenaltyEnabled?: boolean;
frequencyPenaltyEnabled?: boolean;
}
export interface IMemoryAppDetailProps {
avatar: any;
created_by: string;
description: string;
id: string;
name: string;
memory_config: {
cross_languages: string[];
doc_ids: string[];
chat_id: string;
highlight: boolean;
kb_ids: string[];
keyword: boolean;
query_mindmap: boolean;
related_memory: boolean;
rerank_id: string;
use_rerank?: boolean;
similarity_threshold: number;
summary: boolean;
llm_setting: IllmSettingProps & IllmSettingEnableProps;
top_k: number;
use_kg: boolean;
vector_similarity_weight: number;
web_memory: boolean;
chat_settingcross_languages: string[];
meta_data_filter?: {
method: string;
manual: { key: string; op: string; value: string }[];
};
};
tenant_id: string;
update_time: number;
}
export interface MemoryDetailResponse {
code: number;
data: IMemoryAppDetailProps;
message: string;
}
// export type IUpdateMemoryProps = Omit<IMemoryAppDetailProps, 'id'> & {
// id: string;
// };

View File

@ -0,0 +1,32 @@
import { HomeCard } from '@/components/home-card';
import { MoreButton } from '@/components/more-button';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IMemory } from './interface';
import { MemoryDropdown } from './memory-dropdown';
interface IProps {
data: IMemory;
showMemoryRenameModal: (data: IMemory) => void;
}
export function MemoryCard({ data, showMemoryRenameModal }: IProps) {
const { navigateToMemory } = useNavigatePage();
return (
<HomeCard
data={{
name: data?.name,
avatar: data?.avatar,
description: data?.description,
}}
moreDropdown={
<MemoryDropdown
dataset={data}
showMemoryRenameModal={showMemoryRenameModal}
>
<MoreButton></MoreButton>
</MemoryDropdown>
}
onClick={navigateToMemory(data?.id)}
/>
);
}

View File

@ -0,0 +1,74 @@
import {
ConfirmDeleteDialog,
ConfirmDeleteDialogNode,
} from '@/components/confirm-delete-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { PenLine, Trash2 } from 'lucide-react';
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { IMemoryAppProps, useDeleteMemory } from './hooks';
export function MemoryDropdown({
children,
dataset,
showMemoryRenameModal,
}: PropsWithChildren & {
dataset: IMemoryAppProps;
showMemoryRenameModal: (dataset: IMemoryAppProps) => void;
}) {
const { t } = useTranslation();
const { deleteMemory } = useDeleteMemory();
const handleShowChatRenameModal: MouseEventHandler<HTMLDivElement> =
useCallback(
(e) => {
e.stopPropagation();
showMemoryRenameModal(dataset);
},
[dataset, showMemoryRenameModal],
);
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
deleteMemory({ search_id: dataset.id });
}, [dataset.id, deleteMemory]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowChatRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
<DropdownMenuSeparator />
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('deleteModal.delMemory')}
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: dataset.avatar, name: dataset.name }}
name={dataset.name}
/>
),
}}
>
<DropdownMenuItem
className="text-state-error"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
</DropdownMenuContent>
</DropdownMenu>
);
}