diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 1ec2d7dc..ee71bea8 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 14:00:06 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-24 17:48:01 + * @Last Modified time: 2026-03-31 12:25:53 */ import { request } from '@/utils/request' import type { AxiosRequestConfig } from 'axios' @@ -63,8 +63,8 @@ export const getDashboardData = () => { /****************** User Memory APIs *******************************/ export const userMemoryListUrl = '/dashboard/end_users' -export const getUserMemoryList = () => { - return request.get(userMemoryListUrl) +export const getUserMemoryList = (query?: { keyword?: string }) => { + return request.get(userMemoryListUrl, query) } // User Memory - Total end users export const getTotalEndUsers = () => { diff --git a/web/src/components/DebounceSelect/index.tsx b/web/src/components/DebounceSelect/index.tsx new file mode 100644 index 00000000..ab8379ad --- /dev/null +++ b/web/src/components/DebounceSelect/index.tsx @@ -0,0 +1,106 @@ +import { useRef, useState, useCallback, useEffect, type FC } from 'react'; +import { Select, Spin, Avatar } from 'antd'; +import type { SelectProps, DefaultOptionType } from 'antd/es/select'; + +import { request } from '@/utils/request'; + +interface OptionType { + [key: string]: any; +} + +interface ApiResponse { + items?: T[]; +} + +export interface DebounceSelectProps extends Omit { + /** API endpoint URL — mutually exclusive with fetchOptions */ + url?: string; + /** Extra query params merged with the search keyword */ + params?: Record; + /** Key used as option value */ + valueKey?: string; + /** Key used as option label */ + labelKey?: string; + /** Key name sent to the API for the search keyword */ + searchKey?: string; + /** Custom fetch function — mutually exclusive with url */ + fetchOptions?: (search: string | null) => Promise; + /** Transform raw API items before rendering */ + format?: (items: OptionType[]) => OptionType[]; + debounceTimeout?: number; +} + +const DebounceSelect: FC = ({ + url, + params = { page: 1, pagesize: 20 }, + valueKey = 'value', + labelKey = 'label', + searchKey = 'search', + fetchOptions, + format, + debounceTimeout = 300, + ...props +}) => { + const [fetching, setFetching] = useState(false); + const [options, setOptions] = useState([]); + const fetchRef = useRef(0); + + const timerRef = useRef>(); + + // Load initial options on mount + useEffect(() => { + debounceFetcher(null); + }, []); + + const debounceFetcher = useCallback((keyword: string | null) => { + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => { + fetchRef.current += 1; + const fetchId = fetchRef.current; + setOptions([]); + setFetching(true); + + const promise: Promise = fetchOptions + ? fetchOptions(keyword) + : request + .get>(url!, { ...params, [searchKey]: keyword }) + .then((res) => { + const data: OptionType[] = Array.isArray(res) ? res : res?.items || []; + const formatted = format ? format(data) : data.map((item) => ({ + label: item[labelKey], + value: item[valueKey], + avatar: item.avatar, + raw: item, + })); + return formatted; + }); + + promise + .then((newOptions) => { + if (fetchId !== fetchRef.current) return; + setOptions(newOptions); + setFetching(false); + }) + .catch(() => setFetching(false)); + }, debounceTimeout); + }, [url, params, searchKey, fetchOptions, format, valueKey, labelKey, debounceTimeout]); + + return ( + ({ + (items as Data[]).map(item => ({ + ...item, + 'end_user.id': item.end_user?.id, + label: item.end_user?.other_name || item.end_user?.id, value: item.end_user?.id, - label: item?.name, }))} - filterOption={(inputValue, option) => option?.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1} - showSearch={true} - // filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1} placeholder={t('memoryConversation.searchPlaceholder')} style={{ width: '100%', marginBottom: '16px' }} - onChange={setUserId} + onChange={(opt: DefaultOptionType) => setUserId(opt?.value as string)} variant="borderless" className="rb:bg-white rb:rounded-lg" + showSearch /> diff --git a/web/src/views/UserMemory/index.tsx b/web/src/views/UserMemory/index.tsx index 96da9dec..7d5dbdfa 100644 --- a/web/src/views/UserMemory/index.tsx +++ b/web/src/views/UserMemory/index.tsx @@ -2,51 +2,36 @@ * @Author: ZhaoYing * @Date: 2026-02-03 17:53:44 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-26 14:58:48 + * @Last Modified time: 2026-03-31 12:15:59 */ /** * User Memory Page * Displays list of end users with their memory statistics and configuration */ -import { useEffect, useState, useMemo } from 'react'; +import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom' import { Row, Col, Form, Flex, Tooltip } from 'antd'; import type { Data } from './types' -import { getUserMemoryList } from '@/api/memory'; +import { userMemoryListUrl } from '@/api/memory'; import { useUser } from '@/store/user' import RbCard from '@/components/RbCard/Card' import SearchInput from '@/components/SearchInput'; import RbStatistic from '@/components/RbStatistic'; -import BodyWrapper from '@/components/Empty/BodyWrapper' +import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' export default function UserMemory() { const { t } = useTranslation(); const navigate = useNavigate() const { storageType } = useUser() - const [loading, setLoading] = useState(false); - const [data, setData] = useState([]); const [form] = Form.useForm() - const search = Form.useWatch(['search'], form) + const keyword = Form.useWatch(['keyword'], form) - /** Fetch user memory list */ - useEffect(() => { - getData() - }, []); + const scrollListRef = useRef(null) - /** Get data from API */ - const getData = () => { - setLoading(true) - getUserMemoryList().then((res) => { - setData(res as Data[] || []) - }) - .finally(() => { - setLoading(false) - }) - } /** Navigate to user memory detail */ const handleViewDetail = (id: string | number) => { switch (storageType) { @@ -64,25 +49,12 @@ export default function UserMemory() { navigate(`/memory`) } - /** Filter data by search term */ - const filterData = useMemo(() => { - if (search && search.trim() !== '') { - return data.filter((item) => { - const { end_user } = item as Data; - const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id - return name?.includes(search) - }) - } - - return data - }, [search, data]) - return (
- +
- - - {filterData.map((item, index) => { - const { end_user, memory_num, memory_config } = item as Data; - const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id - return ( - - -
{name[0]}
-
{name || '-'}
- } - headerType="border" - headerClassName="rb:h-[48px]! rb:mx-4!" - bodyClassName="rb:py-3! rb:px-4!" - className="rb:cursor-pointer" - onClick={() => handleViewDetail(end_user.id)} - > - - - - - - - - + + + ref={scrollListRef} + url={userMemoryListUrl} + query={{ keyword }} + column={3} + renderItem={(item) => { + const { end_user, memory_num, memory_config } = item as Data; + const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id + return ( + +
{name[0]}
-
- - {t('userMemory.memory_config_name')} -
-
-
{memory_config?.memory_config_name || '-'}
-
-
- - ) - })} -
-
+
{name || '-'}
+ } + headerType="border" + headerClassName="rb:h-[48px]! rb:mx-4!" + bodyClassName="rb:py-3! rb:px-4!" + className="rb:cursor-pointer" + onClick={() => handleViewDetail(end_user.id)} + > + + + + + + + + + +
+ + {t('userMemory.memory_config_name')} +
+
+
{memory_config?.memory_config_name || '-'}
+
+ + ) + }} + />
); } \ No newline at end of file