feat: usage filter & export & dashbord (#3538)

* feat: usage filter & export & dashbord

* adjust ui

* fix tmb scroll

* fix code & selecte all

* merge
This commit is contained in:
heheer 2025-01-23 10:54:30 +08:00 committed by GitHub
parent e009be51e7
commit 0c05add8b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1114 additions and 275 deletions

View File

@ -6,6 +6,23 @@ export type CreateTrainingUsageProps = {
datasetId: string; datasetId: string;
}; };
export type GetTotalPointsProps = {
dateStart: Date;
dateEnd: Date;
teamMemberIds: string[];
sources: UsageSourceEnum[];
unit: 'day' | 'week' | 'month';
};
export type GetUsageProps = {
dateStart: Date;
dateEnd: Date;
sources?: UsageSourceEnum[];
teamMemberIds?: string[];
projectName?: string;
isSelectAllTmb?: boolean;
};
export type ConcatUsageProps = UsageListItemCountType & { export type ConcatUsageProps = UsageListItemCountType & {
teamId: string; teamId: string;
tmbId: string; tmbId: string;

View File

@ -18,30 +18,30 @@ export const UsageSourceMap = {
label: i18nT('common:core.chat.logs.online') label: i18nT('common:core.chat.logs.online')
}, },
[UsageSourceEnum.api]: { [UsageSourceEnum.api]: {
label: 'Api' label: 'API'
}, },
[UsageSourceEnum.shareLink]: { [UsageSourceEnum.shareLink]: {
label: i18nT('common:core.chat.logs.free_login') label: i18nT('common:core.chat.logs.free_login')
}, },
[UsageSourceEnum.training]: { [UsageSourceEnum.training]: {
label: 'dataset.Training Name' label: i18nT('common:dataset.Training Name')
}, },
[UsageSourceEnum.cronJob]: { [UsageSourceEnum.cronJob]: {
label: i18nT('common:cron_job_run_app') label: i18nT('common:cron_job_run_app')
}, },
[UsageSourceEnum.feishu]: { [UsageSourceEnum.feishu]: {
label: i18nT('user:usage.feishu') label: i18nT('account_usage:feishu')
}, },
[UsageSourceEnum.official_account]: { [UsageSourceEnum.official_account]: {
label: i18nT('user:usage.official_account') label: i18nT('account_usage:official_account')
}, },
[UsageSourceEnum.share]: { [UsageSourceEnum.share]: {
label: i18nT('user:usage.share') label: i18nT('account_usage:share')
}, },
[UsageSourceEnum.wecom]: { [UsageSourceEnum.wecom]: {
label: i18nT('user:usage.wecom') label: i18nT('account_usage:wecom')
}, },
[UsageSourceEnum.dingtalk]: { [UsageSourceEnum.dingtalk]: {
label: i18nT('user:usage.dingtalk') label: i18nT('account_usage:dingtalk')
} }
}; };

View File

@ -1,3 +1,4 @@
import { SourceMemberType } from '../../../support/user/type';
import { CreateUsageProps } from './api'; import { CreateUsageProps } from './api';
import { UsageSourceEnum } from './constants'; import { UsageSourceEnum } from './constants';
@ -10,6 +11,7 @@ export type UsageListItemCountType = {
// deprecated // deprecated
tokens?: number; tokens?: number;
}; };
export type UsageListItemType = UsageListItemCountType & { export type UsageListItemType = UsageListItemCountType & {
moduleName: string; moduleName: string;
amount: number; amount: number;
@ -28,4 +30,5 @@ export type UsageItemType = {
source: UsageSchemaType['source']; source: UsageSchemaType['source'];
totalPoints: number; totalPoints: number;
list: UsageSchemaType['list']; list: UsageSchemaType['list'];
sourceMember: SourceMemberType;
}; };

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo, useRef } from 'react'; import React, { useState, useMemo, useRef, useEffect } from 'react';
import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react'; import { Box, Card, Flex, useTheme, useOutsideClick, Button } from '@chakra-ui/react';
import { addDays, format } from 'date-fns'; import { addDays, format } from 'date-fns';
import { type DateRange, DayPicker } from 'react-day-picker'; import { type DateRange, DayPicker } from 'react-day-picker';
@ -14,12 +14,14 @@ const DateRangePicker = ({
defaultDate = { defaultDate = {
from: addDays(new Date(), -30), from: addDays(new Date(), -30),
to: new Date() to: new Date()
} },
dateRange
}: { }: {
onChange?: (date: DateRange) => void; onChange?: (date: DateRange) => void;
onSuccess?: (date: DateRange) => void; onSuccess?: (date: DateRange) => void;
position?: 'bottom' | 'top'; position?: 'bottom' | 'top';
defaultDate?: DateRange; defaultDate?: DateRange;
dateRange?: DateRange;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
@ -27,6 +29,12 @@ const DateRangePicker = ({
const [range, setRange] = useState<DateRange | undefined>(defaultDate); const [range, setRange] = useState<DateRange | undefined>(defaultDate);
const [showSelected, setShowSelected] = useState(false); const [showSelected, setShowSelected] = useState(false);
useEffect(() => {
if (dateRange) {
setRange(dateRange);
}
}, [dateRange]);
const formatSelected = useMemo(() => { const formatSelected = useMemo(() => {
if (range?.from && range.to) { if (range?.from && range.to) {
return `${format(range.from, 'y-MM-dd')} ~ ${format(range.to, 'y-MM-dd')}`; return `${format(range.from, 'y-MM-dd')} ~ ${format(range.to, 'y-MM-dd')}`;
@ -49,7 +57,7 @@ const DateRangePicker = ({
py={1} py={1}
borderRadius={'sm'} borderRadius={'sm'}
cursor={'pointer'} cursor={'pointer'}
bg={'myGray.100'} bg={'myGray.50'}
fontSize={'sm'} fontSize={'sm'}
onClick={() => setShowSelected(true)} onClick={() => setShowSelected(true)}
> >

View File

@ -1,7 +1,7 @@
import { import {
Box, Box,
Button,
ButtonProps, ButtonProps,
Checkbox,
Flex, Flex,
Menu, Menu,
MenuButton, MenuButton,
@ -10,11 +10,12 @@ import {
MenuList, MenuList,
useDisclosure useDisclosure
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useRef } from 'react'; import React, { useMemo, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import MyTag from '../Tag/index'; import MyTag from '../Tag/index';
import MyIcon from '../Icon'; import MyIcon from '../Icon';
import MyAvatar from '../Avatar'; import MyAvatar from '../Avatar';
import { useTranslation } from 'next-i18next';
import { useScrollPagination } from '../../../hooks/useScrollPagination';
export type SelectProps<T = any> = { export type SelectProps<T = any> = {
value: T[]; value: T[];
@ -25,22 +26,31 @@ export type SelectProps<T = any> = {
value: T; value: T;
}[]; }[];
maxH?: number; maxH?: number;
itemWrap?: boolean;
onSelect: (val: T[]) => void; onSelect: (val: T[]) => void;
closeable?: boolean; closeable?: boolean;
showCheckedIcon?: boolean;
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
isSelectAll?: boolean;
setIsSelectAll?: React.Dispatch<React.SetStateAction<boolean>>;
} & Omit<ButtonProps, 'onSelect'>; } & Omit<ButtonProps, 'onSelect'>;
const MultipleSelect = <T = any,>({ const MultipleSelect = <T = any,>({
value = [], value = [],
placeholder, placeholder,
list = [], list = [],
width = '100%',
maxH = 400, maxH = 400,
onSelect, onSelect,
closeable = false, closeable = false,
showCheckedIcon = true,
itemWrap = true,
ScrollData,
isSelectAll,
setIsSelectAll,
...props ...props
}: SelectProps<T>) => { }: SelectProps<T>) => {
const { t } = useTranslation();
const ref = useRef<HTMLButtonElement>(null); const ref = useRef<HTMLButtonElement>(null);
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const menuItemStyles: MenuItemProps = { const menuItemStyles: MenuItemProps = {
borderRadius: 'sm', borderRadius: 'sm',
@ -63,6 +73,71 @@ const MultipleSelect = <T = any,>({
} }
}; };
const onSelectAll = () => {
if (!setIsSelectAll) {
onSelect(value.length === list.length ? [] : list.map((item) => item.value));
return;
}
if (isSelectAll) {
onSelect([]);
}
setIsSelectAll((state) => !state);
};
const ListRender = useMemo(() => {
return (
<>
{list.map((item, i) => (
<MenuItem
key={i}
{...menuItemStyles}
{...((isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value))
? {
color: 'primary.600'
}
: {
color: 'myGray.900'
})}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onclickItem(item.value);
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
>
{!showCheckedIcon && (
<Checkbox
isChecked={
(isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value))
}
/>
)}
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
{showCheckedIcon && (
<Box w={'0.8rem'} lineHeight={1}>
{(isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value) && (
<MyIcon name={'price/right'} w={'1rem'} />
))}
</Box>
)}
</MenuItem>
))}
</>
);
}, [value, list, isSelectAll]);
const isAllSelected = useMemo(
() => (isSelectAll && value.length === 0) || (!isSelectAll && value.length === list.length),
[isSelectAll, value, list]
);
return ( return (
<Box> <Box>
<Menu <Menu
@ -75,12 +150,10 @@ const MultipleSelect = <T = any,>({
closeOnSelect={false} closeOnSelect={false}
> >
<MenuButton <MenuButton
as={Box} as={Flex}
alignItems={'center'}
ref={ref} ref={ref}
width={width}
minH={'40px'}
px={3} px={3}
py={2}
borderRadius={'md'} borderRadius={'md'}
border={'base'} border={'base'}
userSelect={'none'} userSelect={'none'}
@ -88,6 +161,9 @@ const MultipleSelect = <T = any,>({
_active={{ _active={{
transform: 'none' transform: 'none'
}} }}
_hover={{
borderColor: 'primary.300'
}}
{...props} {...props}
{...(isOpen {...(isOpen
? { ? {
@ -102,82 +178,94 @@ const MultipleSelect = <T = any,>({
{placeholder} {placeholder}
</Box> </Box>
) : ( ) : (
<Flex alignItems={'center'} gap={2} flexWrap={'wrap'}> <Flex alignItems={'center'} gap={2}>
{value.map((item, i) => { <Flex
const listItem = list.find((i) => i.value === item); alignItems={'center'}
if (!listItem) return null; gap={2}
flexWrap={itemWrap ? 'wrap' : 'nowrap'}
return ( overflow={'hidden'}
<MyTag className="tag-icon" key={i} colorSchema="blue" type={'borderFill'}> flex={1}
{listItem.label} >
{closeable && ( {isAllSelected ? (
<MyIcon <Box fontSize={'mini'} color={'myGray.900'}>
name={'common/closeLight'} {t('common:common.All')}
ml={1} </Box>
w="0.8rem" ) : (
cursor={'pointer'} (isSelectAll
_hover={{ ? list.filter((item) => !value.includes(item.value))
color: 'red.500' : list.filter((item) => value.includes(item.value))
}} ).map((item, i) => (
onClick={(e) => { <MyTag
console.log(111); className="tag-icon"
e.stopPropagation(); key={i}
onclickItem(item); bg={'primary.100'}
}} color={'primary.700'}
/> type={'fill'}
)} borderRadius={'full'}
</MyTag> px={2}
); py={0.5}
})} flexShrink={0}
>
{item.label}
{closeable && (
<MyIcon
name={'common/closeLight'}
ml={1}
w="0.8rem"
cursor={'pointer'}
_hover={{
color: 'red.500'
}}
onClick={(e) => {
e.stopPropagation();
onclickItem(item.value);
}}
/>
)}
</MyTag>
))
)}
</Flex>
<MyIcon name={'core/chat/chevronDown'} color={'myGray.600'} w={4} h={4} />
</Flex> </Flex>
)} )}
</MenuButton> </MenuButton>
<MenuList <MenuList
className={props.className} className={props.className}
minW={(() => {
const w = ref.current?.clientWidth;
if (w) {
return `${w}px !important`;
}
return Array.isArray(width)
? width.map((item) => `${item} !important`)
: `${width} !important`;
})()}
w={'auto'}
px={'6px'} px={'6px'}
py={'6px'} py={'6px'}
border={'1px solid #fff'} border={'1px solid #fff'}
boxShadow={ boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);' '0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10);'
} }
zIndex={99} zIndex={99}
maxH={'40vh'} maxH={'40vh'}
overflowY={'auto'} overflowY={'auto'}
> >
{list.map((item, i) => ( <MenuItem
<MenuItem {...menuItemStyles}
key={i} color={isAllSelected ? 'primary.600' : 'myGray.900'}
{...menuItemStyles} onClick={(e) => {
{...(value.includes(item.value) e.stopPropagation();
? { e.preventDefault();
color: 'primary.600' onSelectAll();
} }}
: { whiteSpace={'pre-wrap'}
color: 'myGray.900' fontSize={'sm'}
})} gap={2}
onClick={() => onclickItem(item.value)} mb={1}
whiteSpace={'pre-wrap'} >
fontSize={'sm'} {!showCheckedIcon && <Checkbox isChecked={isAllSelected} />}
gap={2} <Box flex={'1 0 0'}>{t('common:common.All')}</Box>
> {showCheckedIcon && (
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
<Box w={'0.8rem'} lineHeight={1}> <Box w={'0.8rem'} lineHeight={1}>
{value.includes(item.value) && <MyIcon name={'price/right'} w={'1rem'} />} {isAllSelected && <MyIcon name={'price/right'} w={'1rem'} />}
</Box> </Box>
</MenuItem> )}
))} </MenuItem>
{ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
</MenuList> </MenuList>
</Menu> </Menu>
</Box> </Box>

View File

@ -66,7 +66,7 @@ const MyTag = ({ children, colorSchema = 'blue', type = 'fill', showDot, ...prop
}, [colorSchema]); }, [colorSchema]);
return ( return (
<Box <Flex
display={'inline-flex'} display={'inline-flex'}
px={2.5} px={2.5}
lineHeight={1} lineHeight={1}
@ -83,7 +83,7 @@ const MyTag = ({ children, colorSchema = 'blue', type = 'fill', showDot, ...prop
> >
{showDot && <Box w={1.5} h={1.5} borderRadius={'md'} bg={theme.color} mr={1.5}></Box>} {showDot && <Box w={1.5} h={1.5} borderRadius={'md'} bg={theme.color} mr={1.5}></Box>}
{children} {children}
</Box> </Flex>
); );
}; };

View File

@ -3,8 +3,14 @@
"all": "all", "all": "all",
"app_name": "Application name", "app_name": "Application name",
"billing_module": "Deduction module", "billing_module": "Deduction module",
"confirm_export": "A total of {{total}} pieces of data were filtered out. Are you sure to export?",
"current_filter_conditions": "Current filter conditions",
"dashboard": "Dashboard",
"details": "Details", "details": "Details",
"dingtalk": "DingTalk",
"duration_seconds": "Duration (seconds)", "duration_seconds": "Duration (seconds)",
"export_confirm": "Export confirmation",
"feishu": "Feishu",
"generation_time": "Generation time", "generation_time": "Generation time",
"input_token_length": "input tokens", "input_token_length": "input tokens",
"member": "member", "member": "member",
@ -12,14 +18,21 @@
"module_name": "module name", "module_name": "module name",
"month": "moon", "month": "moon",
"no_usage_records": "No usage record yet", "no_usage_records": "No usage record yet",
"official_account": "Official Account",
"order_number": "Order number", "order_number": "Order number",
"output_token_length": "output tokens", "output_token_length": "output tokens",
"points": "Points",
"project_name": "Project name", "project_name": "Project name",
"select_member_and_source_first": "Please select members and types first",
"share": "Share Link",
"source": "source", "source": "source",
"start_export": "Export started",
"text_length": "text length", "text_length": "text length",
"token_length": "token length", "token_length": "token length",
"total_points": "AI points consumption", "total_points": "AI points consumption",
"total_points_consumed": "AI points consumption", "total_points_consumed": "AI points consumption",
"usage_detail": "Usage details", "total_usage": "Total Usage",
"user_type": "type" "usage_detail": "Details",
"user_type": "type",
"wecom": "WeCom"
} }

View File

@ -112,10 +112,5 @@
"team.org.org": "Organization", "team.org.org": "Organization",
"team.manage_collaborators": "Manage Collaborators", "team.manage_collaborators": "Manage Collaborators",
"team.no_collaborators": "No Collaborators", "team.no_collaborators": "No Collaborators",
"team.write_role_member": "", "team.write_role_member": ""
"usage.feishu": "Feishu",
"usage.dingtalk": "DingTalk",
"usage.official_account": "Official Account",
"usage.share": "Share Link",
"usage.wecom": "WeCom"
} }

View File

@ -3,8 +3,18 @@
"all": "所有", "all": "所有",
"app_name": "应用名", "app_name": "应用名",
"billing_module": "扣费模块", "billing_module": "扣费模块",
"confirm_export": "共筛选出 {{total}} 条数据,是否确认导出?",
"current_filter_conditions": "当前筛选条件:",
"dashboard": "仪表盘",
"details": "详情", "details": "详情",
"dingtalk": "钉钉",
"duration_seconds": "时长(秒)", "duration_seconds": "时长(秒)",
"every_day": "每天",
"every_month": "每月",
"every_week": "每周",
"export_confirm": "导出确认",
"export_success": "导出成功",
"feishu": "飞书",
"generation_time": "生成时间", "generation_time": "生成时间",
"input_token_length": "输入 tokens", "input_token_length": "输入 tokens",
"member": "成员", "member": "成员",
@ -12,14 +22,21 @@
"module_name": "模块名", "module_name": "模块名",
"month": "月", "month": "月",
"no_usage_records": "暂无使用记录", "no_usage_records": "暂无使用记录",
"official_account": "公众号",
"order_number": "订单号", "order_number": "订单号",
"output_token_length": "输出 tokens", "output_token_length": "输出 tokens",
"points": "积分",
"project_name": "项目名", "project_name": "项目名",
"select_member_and_source_first": "请先选中成员和类型",
"share": "分享链接",
"source": "来源", "source": "来源",
"start_export": "已开始导出",
"text_length": "文本长度", "text_length": "文本长度",
"token_length": "token 长度", "token_length": "token 长度",
"total_points": "AI 积分消耗", "total_points": "AI 积分消耗",
"total_points_consumed": "AI 积分消耗", "total_points_consumed": "AI 积分消耗",
"total_usage": "总消耗",
"usage_detail": "使用详情", "usage_detail": "使用详情",
"user_type": "类型" "user_type": "类型",
"wecom": "企业微信"
} }

View File

@ -112,10 +112,5 @@
"team.org.org": "部门", "team.org.org": "部门",
"team.manage_collaborators": "管理协作者", "team.manage_collaborators": "管理协作者",
"team.no_collaborators": "暂无协作者", "team.no_collaborators": "暂无协作者",
"team.write_role_member": "可写权限", "team.write_role_member": "可写权限"
"usage.feishu": "飞书",
"usage.dingtalk": "钉钉",
"usage.official_account": "公众号",
"usage.share": "分享链接",
"usage.wecom": "企业微信"
} }

View File

@ -3,8 +3,14 @@
"all": "所有", "all": "所有",
"app_name": "應用程式名", "app_name": "應用程式名",
"billing_module": "扣費模組", "billing_module": "扣費模組",
"confirm_export": "共篩選出 {{total}} 條數據,是否確認導出?",
"current_filter_conditions": "當前篩選條件:",
"dashboard": "儀表板",
"details": "詳情", "details": "詳情",
"dingtalk": "釘釘",
"duration_seconds": "時長(秒)", "duration_seconds": "時長(秒)",
"export_confirm": "導出確認",
"feishu": "飛書",
"generation_time": "生成時間", "generation_time": "生成時間",
"input_token_length": "輸入 tokens", "input_token_length": "輸入 tokens",
"member": "成員", "member": "成員",
@ -12,14 +18,21 @@
"module_name": "模組名", "module_name": "模組名",
"month": "月", "month": "月",
"no_usage_records": "暫無使用紀錄", "no_usage_records": "暫無使用紀錄",
"official_account": "公眾號",
"order_number": "訂單編號", "order_number": "訂單編號",
"output_token_length": "輸出 tokens", "output_token_length": "輸出 tokens",
"points": "積分",
"project_name": "專案名", "project_name": "專案名",
"select_member_and_source_first": "請先選取成員和類型",
"share": "分享連結",
"source": "來源", "source": "來源",
"start_export": "已開始匯出",
"text_length": "文字長度", "text_length": "文字長度",
"token_length": "token 長度", "token_length": "token 長度",
"total_points": "AI 積分消耗", "total_points": "AI 積分消耗",
"total_points_consumed": "AI 積分消耗", "total_points_consumed": "AI 積分消耗",
"total_usage": "總消耗",
"usage_detail": "使用詳情", "usage_detail": "使用詳情",
"user_type": "類型" "user_type": "類型",
"wecom": "企業微信"
} }

View File

@ -112,10 +112,5 @@
"team.org.org": "組織", "team.org.org": "組織",
"team.manage_collaborators": "管理協作者", "team.manage_collaborators": "管理協作者",
"team.no_collaborators": "目前沒有協作者", "team.no_collaborators": "目前沒有協作者",
"team.write_role_member": "可寫入權限", "team.write_role_member": "可寫入權限"
"usage.feishu": "飛書",
"usage.dingtalk": "釘釘",
"usage.official_account": "公眾號",
"usage.share": "分享連結",
"usage.wecom": "企業微信"
} }

199
pnpm-lock.yaml generated
View File

@ -22,7 +22,7 @@ importers:
version: 13.3.0 version: 13.3.0
next-i18next: next-i18next:
specifier: 15.3.0 specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
prettier: prettier:
specifier: 3.2.4 specifier: 3.2.4
version: 3.2.4 version: 3.2.4
@ -67,7 +67,7 @@ importers:
version: 4.0.2 version: 4.0.2
next: next:
specifier: 14.2.5 specifier: 14.2.5
version: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
openai: openai:
specifier: 4.61.0 specifier: 4.61.0
version: 4.61.0(encoding@0.1.13) version: 4.61.0(encoding@0.1.13)
@ -210,7 +210,7 @@ importers:
version: 1.4.5-lts.1 version: 1.4.5-lts.1
next: next:
specifier: 14.2.5 specifier: 14.2.5
version: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
nextjs-cors: nextjs-cors:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)) version: 2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))
@ -295,7 +295,7 @@ importers:
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@chakra-ui/next-js': '@chakra-ui/next-js':
specifier: 2.1.5 specifier: 2.1.5
version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) version: 2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)
'@chakra-ui/react': '@chakra-ui/react':
specifier: 2.8.1 specifier: 2.8.1
version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -358,7 +358,7 @@ importers:
version: 4.17.21 version: 4.17.21
next-i18next: next-i18next:
specifier: 15.3.0 specifier: 15.3.0
version: 15.3.0(i18next@23.11.5)(next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) version: 15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
papaparse: papaparse:
specifier: ^5.4.1 specifier: ^5.4.1
version: 5.4.1 version: 5.4.1
@ -558,6 +558,9 @@ importers:
reactflow: reactflow:
specifier: ^11.7.4 specifier: ^11.7.4
version: 11.11.4(@types/react@18.3.1)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 11.11.4(@types/react@18.3.1)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts:
specifier: ^2.15.0
version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
rehype-external-links: rehype-external-links:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
@ -3198,8 +3201,8 @@ packages:
'@tanstack/react-query@4.36.1': '@tanstack/react-query@4.36.1':
resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: 18.3.1
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: 18.3.1
react-native: '*' react-native: '*'
peerDependenciesMeta: peerDependenciesMeta:
react-dom: react-dom:
@ -4330,6 +4333,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
co@4.6.0: co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -4760,6 +4767,9 @@ packages:
supports-color: supports-color:
optional: true optional: true
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decode-named-character-reference@1.0.2: decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
@ -4902,6 +4912,9 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dom-serializer@1.4.1: dom-serializer@1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
@ -5215,6 +5228,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
eventemitter3@5.0.1: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@ -5272,6 +5288,10 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-equals@5.2.0:
resolution: {integrity: sha512-3VpaQYf+CDFdRQfgsb+3vY7XaKjM35WCMoQTTE8h4S/eUkHzyJFOOA/gATYgoLejy4FBrEQD/sXe5Auk4cW/AQ==}
engines: {node: '>=6.0.0'}
fast-fifo@1.3.2: fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
@ -7828,6 +7848,12 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
react-smooth@4.0.4:
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-style-singleton@2.2.1: react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -7849,6 +7875,12 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
react: '>=16.6.0'
react-dom: '>=16.6.0'
react@18.3.1: react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -7878,6 +7910,16 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
recharts-scale@0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
recharts@2.15.0:
resolution: {integrity: sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==}
engines: {node: '>=14'}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
redux@4.2.1: redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
@ -8978,6 +9020,9 @@ packages:
vfile@6.0.2: vfile@6.0.2:
resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==} resolution: {integrity: sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==}
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
vite-node@1.6.0: vite-node@1.6.0:
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@ -10492,6 +10537,14 @@ snapshots:
next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
react: 18.3.1 react: 18.3.1
'@chakra-ui/next-js@2.1.5(@chakra-ui/react@2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)':
dependencies:
'@chakra-ui/react': 2.8.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@emotion/cache': 11.11.0
'@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1)
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
react: 18.3.1
'@chakra-ui/number-input@2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)': '@chakra-ui/number-input@2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@chakra-ui/counter': 2.1.0(react@18.3.1) '@chakra-ui/counter': 2.1.0(react@18.3.1)
@ -13231,7 +13284,7 @@ snapshots:
axios@1.7.7: axios@1.7.7:
dependencies: dependencies:
follow-redirects: 1.15.9(debug@4.3.7) follow-redirects: 1.15.9
form-data: 4.0.1 form-data: 4.0.1
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -13637,6 +13690,8 @@ snapshots:
clone@1.0.4: {} clone@1.0.4: {}
clsx@2.1.1: {}
co@4.6.0: {} co@4.6.0: {}
collapse-white-space@1.0.6: {} collapse-white-space@1.0.6: {}
@ -14078,6 +14133,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decimal.js-light@2.5.1: {}
decode-named-character-reference@1.0.2: decode-named-character-reference@1.0.2:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
@ -14234,6 +14291,11 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.25.7
csstype: 3.1.3
dom-serializer@1.4.1: dom-serializer@1.4.1:
dependencies: dependencies:
domelementtype: 2.3.0 domelementtype: 2.3.0
@ -14731,6 +14793,8 @@ snapshots:
event-target-shim@5.0.1: {} event-target-shim@5.0.1: {}
eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
events@3.3.0: {} events@3.3.0: {}
@ -14837,6 +14901,8 @@ snapshots:
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-equals@5.2.0: {}
fast-fifo@1.3.2: {} fast-fifo@1.3.2: {}
fast-glob@3.3.2: fast-glob@3.3.2:
@ -15012,6 +15078,8 @@ snapshots:
follow-redirects@1.15.6: {} follow-redirects@1.15.6: {}
follow-redirects@1.15.9: {}
follow-redirects@1.15.9(debug@4.3.4): follow-redirects@1.15.9(debug@4.3.4):
optionalDependencies: optionalDependencies:
debug: 4.3.4 debug: 4.3.4
@ -17359,6 +17427,18 @@ snapshots:
react: 18.3.1 react: 18.3.1
react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-i18next@15.3.0(i18next@23.11.5)(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.24.8
'@types/hoist-non-react-statics': 3.3.5
core-js: 3.37.1
hoist-non-react-statics: 3.3.2
i18next: 23.11.5
i18next-fs-backend: 2.3.1
next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
react: 18.3.1
react-i18next: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8): next@14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8):
dependencies: dependencies:
'@next/env': 14.2.5 '@next/env': 14.2.5
@ -17385,10 +17465,36 @@ snapshots:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8):
dependencies:
'@next/env': 14.2.5
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001669
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.1(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 14.2.5
'@next/swc-darwin-x64': 14.2.5
'@next/swc-linux-arm64-gnu': 14.2.5
'@next/swc-linux-arm64-musl': 14.2.5
'@next/swc-linux-x64-gnu': 14.2.5
'@next/swc-linux-x64-musl': 14.2.5
'@next/swc-win32-arm64-msvc': 14.2.5
'@next/swc-win32-ia32-msvc': 14.2.5
'@next/swc-win32-x64-msvc': 14.2.5
sass: 1.77.8
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
nextjs-cors@2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)): nextjs-cors@2.2.0(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)):
dependencies: dependencies:
cors: 2.8.5 cors: 2.8.5
next: 14.2.5(@babel/core@7.24.9)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8) next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.8)
nextjs-node-loader@1.1.5(webpack@5.92.1): nextjs-node-loader@1.1.5(webpack@5.92.1):
dependencies: dependencies:
@ -18097,6 +18203,14 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.1 '@types/react': 18.3.1
react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
fast-equals: 5.2.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1): react-style-singleton@2.2.1(@types/react@18.3.1)(react@18.3.1):
dependencies: dependencies:
get-nonce: 1.0.1 get-nonce: 1.0.1
@ -18124,6 +18238,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@types/react'
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.25.7
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react@18.3.1: react@18.3.1:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@ -18172,6 +18295,23 @@ snapshots:
real-require@0.2.0: {} real-require@0.2.0: {}
recharts-scale@0.4.5:
dependencies:
decimal.js-light: 2.5.1
recharts@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
clsx: 2.1.1
eventemitter3: 4.0.7
lodash: 4.17.21
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 18.3.1
react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts-scale: 0.4.5
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
redux@4.2.1: redux@4.2.1:
dependencies: dependencies:
'@babel/runtime': 7.24.8 '@babel/runtime': 7.24.8
@ -18791,6 +18931,11 @@ snapshots:
'@babel/core': 7.24.9 '@babel/core': 7.24.9
babel-plugin-macros: 3.1.0 babel-plugin-macros: 3.1.0
styled-jsx@5.1.1(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
stylis@4.2.0: {} stylis@4.2.0: {}
stylis@4.3.2: {} stylis@4.3.2: {}
@ -18992,6 +19137,25 @@ snapshots:
ts-dedent@2.2.0: {} ts-dedent@2.2.0: {}
ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3))
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
semver: 7.6.3
typescript: 5.5.3
yargs-parser: 21.1.1
optionalDependencies:
'@babel/core': 7.24.9
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.24.9)
ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3): ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3):
dependencies: dependencies:
bs-logger: 0.2.6 bs-logger: 0.2.6
@ -19377,6 +19541,23 @@ snapshots:
unist-util-stringify-position: 4.0.0 unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2 vfile-message: 4.0.2
victory-vendor@36.9.2:
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-ease': 3.0.2
'@types/d3-interpolate': 3.0.4
'@types/d3-scale': 4.0.8
'@types/d3-shape': 3.1.6
'@types/d3-time': 3.0.3
'@types/d3-timer': 3.0.2
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
vite-node@1.6.0(@types/node@22.7.8)(sass@1.77.8)(terser@5.31.3): vite-node@1.6.0(@types/node@22.7.8)(sass@1.77.8)(terser@5.31.3):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14

View File

@ -21,8 +21,8 @@
"@fastgpt/global": "workspace:*", "@fastgpt/global": "workspace:*",
"@fastgpt/plugins": "workspace:*", "@fastgpt/plugins": "workspace:*",
"@fastgpt/service": "workspace:*", "@fastgpt/service": "workspace:*",
"@fastgpt/web": "workspace:*",
"@fastgpt/templates": "workspace:*", "@fastgpt/templates": "workspace:*",
"@fastgpt/web": "workspace:*",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@node-rs/jieba": "1.10.0", "@node-rs/jieba": "1.10.0",
"@tanstack/react-query": "^4.24.10", "@tanstack/react-query": "^4.24.10",
@ -60,6 +60,7 @@
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.4", "react-textarea-autosize": "^8.5.4",
"reactflow": "^11.7.4", "reactflow": "^11.7.4",
"recharts": "^2.15.0",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.0",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",

View File

@ -32,6 +32,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { usePagination } from '@fastgpt/web/hooks/usePagination'; import { usePagination } from '@fastgpt/web/hooks/usePagination';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const BillTable = () => { const BillTable = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
@ -177,6 +178,7 @@ export default BillTable;
function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) { function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<MyModal <MyModal
isOpen={true} isOpen={true}

View File

@ -0,0 +1,88 @@
import { downloadFetch } from '@/web/common/system/utils';
import { Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
import { formatTime2YMD } from '@fastgpt/global/common/string/time';
import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
export type ExportModalParams = {
dateStart: Date;
dateEnd: Date;
sources: UsageSourceEnum[];
teamMemberIds: string[];
teamMemberNames: string[];
isSelectAllTmb: boolean;
projectName: string;
};
const ExportModal = ({
onClose,
params,
memberTotal,
total
}: {
onClose: () => void;
params: ExportModalParams;
memberTotal: number;
total: number;
}) => {
const { t } = useTranslation();
const {
teamMemberIds,
teamMemberNames,
isSelectAllTmb,
sources,
dateStart,
dateEnd,
projectName
} = params;
const { runAsync: exportUsage, loading } = useRequest2(
async () => {
const searchParams = new URLSearchParams();
searchParams.set('dateStart', dateStart.toISOString());
searchParams.set('dateEnd', dateEnd.toISOString());
sources.forEach((source) => searchParams.append('sources', source.toString()));
teamMemberIds.forEach((tmbId) => searchParams.append('teamMemberIds', tmbId));
searchParams.set('isSelectAllTmb', isSelectAllTmb.toString());
searchParams.set('projectName', projectName);
await downloadFetch({
url: `/api/proApi/support/wallet/usage/exportUsage?${searchParams}`,
filename: `usage.csv`
});
},
{
successToast: t('account_usage:start_export')
}
);
return (
<MyModal title={t('account_usage:export_confirm')} iconSrc="export" iconColor={'primary.600'}>
<ModalBody>
<Flex mb={4}>{t('account_usage:current_filter_conditions')}</Flex>
<Flex>
{`${t('common:user.Time')}: ${formatTime2YMD(dateStart)} ~ ${formatTime2YMD(dateEnd)}`}
</Flex>
<Flex>{`${t('common:user.team.Member')}(${memberTotal}): ${teamMemberNames.join(', ')}`}</Flex>
<Flex>
{`${t('common:user.type')}: ${sources.map((item) => t(UsageSourceMap[item].label as any)).join(', ')}`}
</Flex>
<Flex>{`${t('common:user.Application Name')}: ${projectName}`}</Flex>
<Flex mt={4}>{t('account_usage:confirm_export', { total })}</Flex>
</ModalBody>
<ModalFooter gap={2}>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button onClick={exportUsage} isLoading={loading}>
{t('common:Export')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default ExportModal;

View File

@ -0,0 +1,164 @@
import { getTotalPoints } from '@/web/support/wallet/usage/api';
import { Box, Flex } from '@chakra-ui/react';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { addDays } from 'date-fns';
import { useTranslation } from 'next-i18next';
import React, { useEffect, useMemo } from 'react';
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
TooltipProps
} from 'recharts';
import { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
import { UnitType } from '../index';
export type usageFormType = {
date: string;
totalPoints: number;
};
const CustomTooltip = ({ active, payload }: TooltipProps<ValueType, NameType>) => {
const data = payload?.[0]?.payload as usageFormType;
const { t } = useTranslation();
if (active && data) {
return (
<Box
bg={'white'}
p={3}
borderRadius={'md'}
border={'0.5px solid'}
borderColor={'myGray.200'}
boxShadow={
'0px 24px 48px -12px rgba(19, 51, 107, 0.20), 0px 0px 1px 0px rgba(19, 51, 107, 0.20)'
}
>
<Box fontSize={'mini'} color={'myGray.600'} mb={3}>
{data.date}
</Box>
<Box fontSize={'14px'} color={'myGray.900'} fontWeight={'medium'}>
{`${formatNumber(data.totalPoints)} ${t('account_usage:points')}`}
</Box>
</Box>
);
}
return null;
};
const UsageForm = ({
dateRange,
selectTmbIds,
usageSources,
unit,
Tabs,
Selectors
}: {
dateRange: DateRangeType;
selectTmbIds: string[];
usageSources: UsageSourceEnum[];
unit: UnitType;
Tabs: React.ReactNode;
Selectors: React.ReactNode;
}) => {
const { t } = useTranslation();
const {
run: getTotalPointsData,
data: totalPoints,
loading: totalPointsLoading
} = useRequest2(
() =>
getTotalPoints({
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
teamMemberIds: selectTmbIds,
sources: usageSources,
unit
}),
{
manual: true
}
);
const totalUsage = useMemo(() => {
return totalPoints?.reduce((acc, curr) => acc + curr.totalPoints, 0);
}, [totalPoints]);
useEffect(() => {
if (selectTmbIds.length === 0 || usageSources.length === 0) return;
getTotalPointsData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [usageSources, selectTmbIds.length, dateRange, unit]);
return (
<>
<Box>{Tabs}</Box>
<Box>{Selectors}</Box>
<MyBox isLoading={totalPointsLoading}>
<Flex fontSize={'20px'} fontWeight={'medium'} my={6}>
<Box color={'black'}>{`${t('account_usage:total_usage')}:`}</Box>
<Box color={'primary.600'} ml={2}>
{`${formatNumber(totalUsage || 0)} ${t('account_usage:points')}`}
</Box>
</Flex>
<Flex mb={4} fontSize={'mini'} color={'myGray.500'} fontWeight={'medium'}>
{t('account_usage:points')}
</Flex>
<ResponsiveContainer width="100%" height={424}>
<LineChart data={totalPoints} margin={{ top: 10, right: 30, left: -12, bottom: 0 }}>
<XAxis
dataKey="date"
padding={{ left: 40, right: 40 }}
tickMargin={10}
tickSize={0}
tick={{ fontSize: '12px', color: '#667085', fontWeight: '500' }}
/>
<YAxis
axisLine={false}
tickSize={0}
tickMargin={12}
tick={{ fontSize: '12px', color: '#667085', fontWeight: '500' }}
/>
<CartesianGrid
strokeDasharray="3 3"
verticalCoordinatesGenerator={(props) => {
const { width } = props;
if (width < 500) {
return [width * 0.2, width * 0.4, width * 0.6, width * 0.8];
} else {
return [
width * 0.125,
width * 0.25,
width * 0.375,
width * 0.5,
width * 0.625,
width * 0.75,
width * 0.875
];
}
}}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="totalPoints"
stroke="#5E8FFF"
strokeWidth={1.5}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</MyBox>
</>
);
};
export default React.memo(UsageForm);

View File

@ -0,0 +1,188 @@
import {
Box,
Button,
Flex,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr
} from '@chakra-ui/react';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyBox from '@fastgpt/web/components/common/MyBox';
import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { getUserUsages } from '@/web/support/wallet/usage/api';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import { addDays } from 'date-fns';
import { ExportModalParams } from './ExportModal';
import dynamic from 'next/dynamic';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
const UsageDetail = dynamic(() => import('./UsageDetail'));
const ExportModal = dynamic(() => import('./ExportModal'));
const UsageTableList = ({
dateRange,
selectTmbIds,
usageSources,
projectName,
members,
memberTotal,
isSelectAllTmb,
Tabs,
Selectors
}: {
dateRange: DateRangeType;
selectTmbIds: string[];
usageSources: UsageSourceEnum[];
projectName: string;
members: TeamMemberItemType[];
memberTotal: number;
isSelectAllTmb: boolean;
Tabs: React.ReactNode;
Selectors: React.ReactNode;
}) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const { toast } = useToast();
const {
data: usages,
isLoading,
Pagination,
getData,
total
} = usePagination(getUserUsages, {
pageSize: isPc ? 20 : 10,
params: {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
sources: usageSources,
teamMemberIds: selectTmbIds,
isSelectAllTmb,
projectName
},
defaultRequest: false
});
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const [currentParams, setCurrentParams] = useState<ExportModalParams | null>(null);
useEffect(() => {
if ((!isSelectAllTmb && selectTmbIds.length === 0) || usageSources.length === 0) return;
getData(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [usageSources, selectTmbIds.length, projectName, dateRange, isSelectAllTmb]);
return (
<>
<Box>{Tabs}</Box>
<Flex flexDir={['column', 'row']} w={'100%'} alignItems={['flex-end', 'center']}>
<Box>{Selectors}</Box>
<Box flex={'1'} />
<Button
size={'md'}
onClick={() => {
if ((selectTmbIds.length === 0 && !isSelectAllTmb) || usageSources.length === 0) {
return toast({
status: 'warning',
title: t('account_usage:select_member_and_source_first')
});
}
setCurrentParams({
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
sources: usageSources,
teamMemberIds: selectTmbIds,
teamMemberNames: members
.filter((item) =>
isSelectAllTmb
? !selectTmbIds.includes(item.tmbId)
: selectTmbIds.includes(item.tmbId)
)
.map((item) => item.memberName),
isSelectAllTmb,
projectName
});
}}
>
{t('common:Export')}
</Button>
</Flex>
<MyBox position={'relative'} overflowY={'auto'} mt={3} flex={1} isLoading={isLoading}>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>{t('common:user.Time')}</Th>
<Th>{t('account_usage:member')}</Th>
<Th>{t('account_usage:user_type')}</Th>
<Th>{t('account_usage:project_name')}</Th>
<Th>{t('account_usage:total_points')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{usages.map((item) => (
<Tr key={item.id}>
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>
<Flex alignItems={'center'} color={'myGray.500'}>
<Avatar src={item.sourceMember.avatar} w={'20px'} mr={1} rounded={'full'} />
{item.sourceMember.name}
</Flex>
</Td>
<Td>{t(UsageSourceMap[item.source]?.label as any) || '-'}</Td>
<Td>{t(item.appName as any) || '-'}</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button
size={'sm'}
variant={'whitePrimary'}
onClick={() => setUsageDetail(item)}
>
{t('account_usage:details')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && usages.length === 0 && (
<EmptyTip text={t('account_usage:no_usage_records')}></EmptyTip>
)}
</TableContainer>
</MyBox>
<Flex mt={3} justifyContent={'center'}>
<Pagination />
</Flex>
{!!usageDetail && (
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
)}
{!!currentParams && (
<ExportModal
onClose={() => setCurrentParams(null)}
params={currentParams}
memberTotal={isSelectAllTmb ? memberTotal - selectTmbIds.length : selectTmbIds.length}
total={total}
/>
)}
</>
);
};
export default UsageTableList;

View File

@ -1,76 +1,66 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { import { Flex, Box } from '@chakra-ui/react';
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box,
Button
} from '@chakra-ui/react';
import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants'; import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import { getUserUsages } from '@/web/support/wallet/usage/api';
import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import dayjs from 'dayjs';
import DateRangePicker, { import DateRangePicker, {
type DateRangeType type DateRangeType
} from '@fastgpt/web/components/common/DateRangePicker'; } from '@fastgpt/web/components/common/DateRangePicker';
import { addDays } from 'date-fns'; import { addDays, startOfMonth, startOfWeek } from 'date-fns';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import AccountContainer from '../components/AccountContainer'; import AccountContainer from '../components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs'; import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getTeamMembers } from '@/web/support/user/team/api'; import { getTeamMembers } from '@/web/support/user/team/api';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import UsageForm from './components/UsageForm';
import UsageTableList from './components/UsageTable';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRouter } from 'next/router';
const UsageDetail = dynamic(() => import('./UsageDetail')); export enum UsageTabEnum {
detail = 'detail',
dashboard = 'dashboard'
}
export type UnitType = 'day' | 'week' | 'month';
const UsageTable = () => { const UsageTable = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { Loading } = useLoading(); const { userInfo } = useUserStore();
const router = useRouter();
const { usageTab = UsageTabEnum.detail } = router.query as { usageTab: `${UsageTabEnum}` };
const { data: members, ScrollData, total: memberTotal } = useScrollPagination(getTeamMembers, {});
const [dateRange, setDateRange] = useState<DateRangeType>({ const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7), from: addDays(new Date(), -7),
to: new Date() to: new Date()
}); });
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>(''); const [selectTmbIds, setSelectTmbIds] = useState<string[]>([]);
const { isPc } = useSystem(); const [usageSources, setUsageSources] = useState<UsageSourceEnum[]>(
const { userInfo } = useUserStore(); Object.values(UsageSourceEnum)
const [usageDetail, setUsageDetail] = useState<UsageItemType>(); );
const [isSelectAllTmb, setIsSelectAllTmb] = useState<boolean>(true);
const [unit, setUnit] = useState<UnitType>('day');
const [projectName, setProjectName] = useState<string>('');
const [inputValue, setInputValue] = useState('');
const sourceList = useMemo( const sourceList = useMemo(
() => () =>
[ Object.entries(UsageSourceMap).map(([key, value]) => ({
{ label: t('account_usage:all'), value: '' }, label: t(value.label as any),
...Object.entries(UsageSourceMap).map(([key, value]) => ({ value: key
label: t(value.label as any), })),
value: key
}))
] as {
label: never;
value: UsageSourceEnum | '';
}[],
[t] [t]
); );
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, {});
const tmbList = useMemo( const tmbList = useMemo(
() => () =>
members.map((item) => ({ members.map((item) => ({
label: ( label: (
<Flex alignItems={'center'}> <Flex alignItems={'center'} color={'myGray.500'}>
<Avatar src={item.avatar} w={'16px'} mr={1} /> <Avatar src={item.avatar} w={'20px'} mr={1} rounded={'full'} />
{item.memberName} {item.memberName}
</Flex> </Flex>
), ),
@ -79,122 +69,198 @@ const UsageTable = () => {
[members] [members]
); );
const { const Tabs = useMemo(
data: usages, () => (
isLoading, <FillRowTabs
Pagination, list={[
getData { label: t('account_usage:usage_detail'), value: 'detail' },
} = usePagination(getUserUsages, { { label: t('account_usage:dashboard'), value: 'dashboard' }
pageSize: isPc ? 20 : 10, ]}
params: { px={'1rem'}
dateStart: dateRange.from || new Date(), value={usageTab}
dateEnd: addDays(dateRange.to || new Date(), 1), onChange={(e) => {
source: usageSource as UsageSourceEnum, router.replace({
teamMemberId: selectTmbId ?? '' query: {
}, ...router.query,
defaultRequest: false usageTab: e
}); }
});
}}
/>
),
[router, t, usageTab]
);
const Selectors = useMemo(
() => (
<Flex mt={4}>
<Flex alignItems={'center'}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} mr={4}>
{t('common:user.Time')}
</Box>
<DateRangePicker
defaultDate={dateRange}
dateRange={dateRange}
position="bottom"
onChange={setDateRange}
/>
{usageTab === UsageTabEnum.dashboard && (
<MySelect
bg={'myGray.50'}
minH={'32px'}
height={'32px'}
fontSize={'mini'}
ml={1}
list={[
{ label: t('account_usage:every_day'), value: 'day' },
{ label: t('account_usage:every_week'), value: 'week' },
{ label: t('account_usage:every_month'), value: 'month' }
]}
value={unit}
onchange={(val) => {
if (!dateRange.from) return dateRange;
switch (val) {
case 'week':
setDateRange({
from: startOfWeek(dateRange.from, { weekStartsOn: 1 }),
to: dateRange.to
});
break;
case 'month':
setDateRange({
from: startOfMonth(dateRange.from),
to: dateRange.to
});
break;
default:
break;
}
setUnit(val as 'day' | 'week' | 'month');
}}
/>
)}
</Flex>
{tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && (
<Flex alignItems={'center'} ml={6}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} mr={4}>
{t('account_usage:member')}
</Box>
<Box>
<MultipleSelect<string>
list={tmbList}
value={selectTmbIds}
onSelect={(val) => {
setSelectTmbIds(val as string[]);
}}
itemWrap={false}
height={'32px'}
bg={'myGray.50'}
w={'160px'}
showCheckedIcon={false}
ScrollData={ScrollData}
isSelectAll={isSelectAllTmb}
setIsSelectAll={setIsSelectAllTmb}
/>
</Box>
</Flex>
)}
<Flex alignItems={'center'} ml={6}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} mr={4}>
{t('common:user.type')}
</Box>
<Box>
<MultipleSelect<string>
list={sourceList}
value={usageSources}
onSelect={(val) => setUsageSources(val as UsageSourceEnum[])}
itemWrap={false}
height={'32px'}
bg={'myGray.50'}
w={'160px'}
showCheckedIcon={false}
/>
</Box>
</Flex>
{usageTab === UsageTabEnum.detail && (
<Flex alignItems={'center'} ml={6}>
<Box
fontSize={'mini'}
fontWeight={'medium'}
color={'myGray.900'}
mr={4}
whiteSpace={'nowrap'}
>
{t('common:user.Application Name')}
</Box>
<SearchInput
placeholder={t('common:user.Application Name')}
w={'160px'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</Flex>
)}
</Flex>
),
[
dateRange,
selectTmbIds,
sourceList,
t,
tmbList,
unit,
usageSources,
usageTab,
inputValue,
isSelectAllTmb
]
);
useEffect(() => { useEffect(() => {
getData(1); const timer = setTimeout(() => {
}, [usageSource, selectTmbId]); setProjectName(inputValue);
}, 300);
return () => clearTimeout(timer);
}, [inputValue]);
return ( return (
<AccountContainer> <AccountContainer>
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}> <Box
<Flex px={[3, 8]}
flexDir={['column', 'row']} pt={[0, 8]}
gap={2} pb={[0, 4]}
w={'100%'} h={'full'}
px={[3, 8]} overflow={'hidden'}
alignItems={['flex-end', 'center']} display={'flex'}
> flexDirection={'column'}
{tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && ( >
<Flex alignItems={'center'}> {usageTab === UsageTabEnum.detail && (
<Box mr={2} flexShrink={0}> <UsageTableList
{t('account_usage:member')} dateRange={dateRange}
</Box> selectTmbIds={selectTmbIds}
<MySelect usageSources={usageSources}
size={'sm'} projectName={projectName}
minW={'100px'} members={members}
ScrollData={ScrollData} memberTotal={memberTotal}
list={tmbList} isSelectAllTmb={isSelectAllTmb}
value={selectTmbId} Tabs={Tabs}
onchange={setSelectTmbId} Selectors={Selectors}
/> />
</Flex>
)}
<Box flex={'1'} />
<Flex alignItems={'center'} gap={3}>
<DateRangePicker
defaultDate={dateRange}
position="bottom"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Flex>
</Flex>
<TableContainer
mt={2}
px={[3, 8]}
position={'relative'}
flex={'1 0 0'}
h={0}
overflowY={'auto'}
>
<Table>
<Thead>
<Tr>
{/* <Th>{t('account_usage:user.team.Member Name')}</Th> */}
<Th>{t('account_usage:user_type')}</Th>
<Th>
<MySelect<UsageSourceEnum | ''>
list={sourceList}
value={usageSource}
size={'sm'}
onchange={(e) => {
setUsageSource(e);
}}
w={'130px'}
></MySelect>
</Th>
<Th>{t('account_usage:project_name')}</Th>
<Th>{t('account_usage:total_points')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{usages.map((item) => (
<Tr key={item.id}>
{/* <Td>{item.memberName}</Td> */}
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>{t(UsageSourceMap[item.source]?.label as any) || '-'}</Td>
<Td>{t(item.appName as any) || '-'}</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button
size={'sm'}
variant={'whitePrimary'}
onClick={() => setUsageDetail(item)}
>
{t('account_usage:details')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && usages.length === 0 && (
<EmptyTip text={t('account_usage:no_usage_records')}></EmptyTip>
)}
</TableContainer>
<Loading loading={isLoading} fixed={false} />
{!!usageDetail && (
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
)} )}
</Flex> {usageTab === UsageTabEnum.dashboard && (
<UsageForm
dateRange={dateRange}
selectTmbIds={selectTmbIds}
usageSources={usageSources}
unit={unit}
Tabs={Tabs}
Selectors={Selectors}
/>
)}
</Box>
</AccountContainer> </AccountContainer>
); );
}; };

View File

@ -359,6 +359,8 @@ const InputTypeConfig = ({
<MultipleSelect<WorkflowIOValueTypeEnum> <MultipleSelect<WorkflowIOValueTypeEnum>
list={valueTypeSelectList} list={valueTypeSelectList}
bg={'myGray.50'} bg={'myGray.50'}
minH={'40px'}
py={2}
value={selectValueTypeList || []} value={selectValueTypeList || []}
onSelect={(e) => { onSelect={(e) => {
setValue('customInputConfig.selectValueTypeList', e); setValue('customInputConfig.selectValueTypeList', e);

View File

@ -1,17 +1,20 @@
import { POST } from '@/web/common/api/request'; import { POST } from '@/web/common/api/request';
import { CreateTrainingUsageProps } from '@fastgpt/global/support/wallet/usage/api.d'; import {
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; CreateTrainingUsageProps,
GetTotalPointsProps,
GetUsageProps
} from '@fastgpt/global/support/wallet/usage/api.d';
import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type'; import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
export const getUserUsages = ( export const getUserUsages = (data: PaginationProps<GetUsageProps>) =>
data: PaginationProps<{ POST<PaginationResponse<UsageItemType>>(`/proApi/support/wallet/usage/getUsage`, data);
dateStart: Date;
dateEnd: Date; export const getTotalPoints = (data: GetTotalPointsProps) =>
source?: UsageSourceEnum; POST<{ totalPoints: number; date: string }[]>(
teamMemberId: string; `/proApi/support/wallet/usage/getTotalPoints`,
}> data
) => POST<PaginationResponse<UsageItemType>>(`/proApi/support/wallet/usage/getUsage`, data); );
export const postCreateTrainingUsage = (data: CreateTrainingUsageProps) => export const postCreateTrainingUsage = (data: CreateTrainingUsageProps) =>
POST<string>(`/support/wallet/usage/createTrainingUsage`, data); POST<string>(`/support/wallet/usage/createTrainingUsage`, data);