@lobehub/lobehub 2.0.0-next.38 → 2.0.0-next.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
  3. package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
  4. package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
  5. package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
  6. package/changelog/v1.json +5 -0
  7. package/locales/ar/auth.json +45 -1
  8. package/locales/bg-BG/auth.json +45 -1
  9. package/locales/de-DE/auth.json +45 -1
  10. package/locales/en-US/auth.json +45 -1
  11. package/locales/es-ES/auth.json +45 -1
  12. package/locales/fa-IR/auth.json +45 -1
  13. package/locales/fr-FR/auth.json +45 -1
  14. package/locales/it-IT/auth.json +45 -1
  15. package/locales/ja-JP/auth.json +45 -1
  16. package/locales/ko-KR/auth.json +45 -1
  17. package/locales/nl-NL/auth.json +45 -1
  18. package/locales/pl-PL/auth.json +45 -1
  19. package/locales/pt-BR/auth.json +45 -1
  20. package/locales/ru-RU/auth.json +45 -1
  21. package/locales/tr-TR/auth.json +45 -1
  22. package/locales/vi-VN/auth.json +45 -1
  23. package/locales/zh-CN/auth.json +45 -1
  24. package/locales/zh-TW/auth.json +45 -1
  25. package/package.json +1 -1
  26. package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
  27. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
  28. package/packages/obervability-otel/package.json +3 -1
  29. package/packages/obervability-otel/src/api.ts +2 -0
  30. package/packages/obervability-otel/src/trpc/convention.ts +16 -0
  31. package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
  32. package/packages/obervability-otel/src/trpc/index.ts +62 -0
  33. package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
  34. package/packages/types/src/usage/usageRecord.ts +54 -0
  35. package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
  36. package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
  37. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
  38. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
  39. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
  40. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
  41. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
  42. package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
  43. package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
  44. package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
  45. package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
  46. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
  47. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
  48. package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
  49. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
  50. package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
  51. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
  52. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
  53. package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
  54. package/src/features/Conversation/Messages/Group/index.tsx +7 -2
  55. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
  56. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  57. package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
  58. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
  59. package/src/features/PluginsUI/Render/index.tsx +17 -0
  60. package/src/libs/mcp/client.ts +3 -2
  61. package/src/libs/mcp/types.ts +71 -0
  62. package/src/libs/trpc/lambda/index.ts +5 -2
  63. package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
  64. package/src/locales/default/auth.ts +44 -0
  65. package/src/locales/default/chat.ts +1 -0
  66. package/src/server/routers/desktop/mcp.ts +1 -3
  67. package/src/server/routers/lambda/index.ts +2 -0
  68. package/src/server/routers/lambda/usage.ts +36 -0
  69. package/src/server/routers/tools/mcp.ts +1 -3
  70. package/src/server/services/mcp/index.test.ts +28 -15
  71. package/src/server/services/mcp/index.ts +29 -18
  72. package/src/server/services/usage/index.test.ts +310 -0
  73. package/src/server/services/usage/index.ts +164 -0
  74. package/src/services/chat/contextEngineering.test.ts +4 -0
  75. package/src/services/mcp.test.ts +7 -1
  76. package/src/services/mcp.ts +13 -12
  77. package/src/services/usage.ts +13 -0
  78. package/src/store/chat/agents/createAgentExecutors.ts +2 -3
  79. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
  80. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
  81. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
  82. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
  83. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
  84. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  85. package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
  86. package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
  87. package/src/store/chat/slices/message/initialState.ts +5 -0
  88. package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
  89. package/src/store/chat/slices/message/selectors/chat.ts +0 -2
  90. package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
  91. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
  92. package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
  93. package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
  94. package/src/store/chat/slices/plugin/action.test.ts +4 -4
  95. package/src/store/chat/slices/plugin/actions/index.ts +39 -0
  96. package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
  97. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
  98. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
  99. package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
  100. package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
  101. package/src/store/chat/store.ts +1 -1
  102. package/src/store/global/initialState.ts +1 -0
  103. package/src/store/chat/slices/plugin/action.ts +0 -539
@@ -0,0 +1,126 @@
1
+ import { ModelIcon, ProviderIcon } from '@lobehub/icons';
2
+ import { ActionIcon, Modal } from '@lobehub/ui';
3
+ import { useTheme } from 'antd-style';
4
+ import { MaximizeIcon } from 'lucide-react';
5
+ import { memo, useMemo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import StatisticCard from '@/components/StatisticCard';
10
+ import TitleWithPercentage from '@/components/StatisticCard/TitleWithPercentage';
11
+ import { UsageLog } from '@/types/usage/usageRecord';
12
+ import { formatNumber } from '@/utils/format';
13
+
14
+ import { GroupBy, UsageChartProps } from '../../../Client';
15
+ import ModelTable from './ModelTable';
16
+
17
+ const computeList = (data: UsageLog[], groupBy: GroupBy): string[] => {
18
+ if (!data || data?.length === 0) return [];
19
+
20
+ return Array.from(
21
+ data.reduce((acc, log) => {
22
+ if (log.records) {
23
+ for (const item of log.records) {
24
+ if (groupBy === GroupBy.Model && item.model?.length !== 0) {
25
+ acc.add(item.model);
26
+ }
27
+ if (groupBy === GroupBy.Provider && item.provider?.length !== 0) {
28
+ acc.add(item.provider);
29
+ }
30
+ }
31
+ }
32
+ return acc;
33
+ }, new Set<string>()),
34
+ );
35
+ };
36
+
37
+ const ActiveModels = memo<UsageChartProps>(({ data, isLoading, groupBy }) => {
38
+ const { t } = useTranslation('auth');
39
+ const theme = useTheme();
40
+
41
+ const [open, setOpen] = useState(false);
42
+
43
+ const iconList = useMemo(
44
+ () => computeList(data || [], groupBy || GroupBy.Model),
45
+ [data, groupBy],
46
+ );
47
+
48
+ return (
49
+ <>
50
+ <StatisticCard
51
+ extra={
52
+ <ActionIcon
53
+ icon={MaximizeIcon}
54
+ onClick={() => setOpen(true)}
55
+ title={
56
+ groupBy === GroupBy.Model
57
+ ? t('usage.activeModels.modelTable')
58
+ : t('usage.activeModels.providerTable')
59
+ }
60
+ />
61
+ }
62
+ key={groupBy}
63
+ loading={isLoading}
64
+ statistic={{
65
+ description: (
66
+ <Flexbox horizontal wrap={'wrap'}>
67
+ {iconList.map((item, i) => {
68
+ if (!item) return null;
69
+ return groupBy === GroupBy.Model ? (
70
+ <ModelIcon
71
+ key={item}
72
+ model={item}
73
+ size={18}
74
+ style={{
75
+ border: `2px solid ${theme.colorBgContainer}`,
76
+ boxSizing: 'content-box',
77
+ marginRight: -8,
78
+ zIndex: i + 1,
79
+ }}
80
+ />
81
+ ) : (
82
+ <ProviderIcon
83
+ key={item}
84
+ provider={item}
85
+ size={18}
86
+ style={{
87
+ border: `2px solid ${theme.colorBgContainer}`,
88
+ boxSizing: 'content-box',
89
+ marginRight: -8,
90
+ zIndex: i + 1,
91
+ }}
92
+ />
93
+ );
94
+ })}
95
+ </Flexbox>
96
+ ),
97
+ precision: 0,
98
+ value: formatNumber(iconList?.length ?? 0),
99
+ }}
100
+ title={
101
+ <TitleWithPercentage
102
+ title={
103
+ groupBy === GroupBy.Model
104
+ ? t('usage.activeModels.models')
105
+ : t('usage.activeModels.providers')
106
+ }
107
+ />
108
+ }
109
+ />
110
+ <Modal
111
+ footer={null}
112
+ onCancel={() => setOpen(false)}
113
+ open={open}
114
+ title={
115
+ groupBy === GroupBy.Model
116
+ ? t('usage.activeModels.modelTable')
117
+ : t('usage.activeModels.providerTable')
118
+ }
119
+ >
120
+ <ModelTable data={data} groupBy={groupBy} isLoading={isLoading} />
121
+ </Modal>
122
+ </>
123
+ );
124
+ });
125
+
126
+ export default ActiveModels;
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { useTheme } from 'antd-style';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import Statistic from '@/components/Statistic';
8
+ import StatisticCard from '@/components/StatisticCard';
9
+ import TitleWithPercentage from '@/components/StatisticCard/TitleWithPercentage';
10
+ import { UsageLog } from '@/types/usage/usageRecord';
11
+ import { formatNumber } from '@/utils/format';
12
+
13
+ import { UsageChartProps } from '../../Client';
14
+
15
+ const computeMonth = (
16
+ data: UsageLog[],
17
+ ): {
18
+ calls: number | string;
19
+ spend: number | string;
20
+ } => {
21
+ if (!data || data?.length === 0) return { calls: 0, spend: 0 };
22
+
23
+ const spend = data.reduce((acc, log) => acc + (log.totalSpend || 0), 0);
24
+ const calls = data.reduce((acc, log) => acc + (log.records?.length ?? 0), 0);
25
+
26
+ return {
27
+ calls: formatNumber(calls),
28
+ spend: formatNumber(spend),
29
+ };
30
+ };
31
+
32
+ const MonthSpend = memo<UsageChartProps>(({ data, isLoading }) => {
33
+ const { t } = useTranslation('auth');
34
+ const theme = useTheme();
35
+
36
+ const { spend, calls } = computeMonth(data || []);
37
+
38
+ return (
39
+ <StatisticCard
40
+ highlight={theme.blue}
41
+ loading={isLoading}
42
+ statistic={{
43
+ description: <Statistic title={t('usage.cards.month.modelCalls')} value={calls} />,
44
+ precision: 2,
45
+ prefix: '$',
46
+ value: spend,
47
+ }}
48
+ title={<TitleWithPercentage title={t('usage.cards.month.title')} />}
49
+ />
50
+ );
51
+ });
52
+
53
+ export default MonthSpend;
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { useTheme } from 'antd-style';
4
+ import dayjs from 'dayjs';
5
+ import utc from 'dayjs/plugin/utc';
6
+ import isToday from 'dayjs/plugin/isToday';
7
+ import isYesterday from 'dayjs/plugin/isYesterday';
8
+ import { memo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+
11
+ import Statistic from '@/components/Statistic';
12
+ import StatisticCard from '@/components/StatisticCard';
13
+ import TitleWithPercentage from '@/components/StatisticCard/TitleWithPercentage';
14
+ import { UsageLog } from '@/types/usage/usageRecord';
15
+ import { formatNumber } from '@/utils/format';
16
+
17
+ import { UsageChartProps } from '../../Client';
18
+
19
+ dayjs.extend(utc);
20
+ dayjs.extend(isToday);
21
+ dayjs.extend(isYesterday);
22
+
23
+ const computeSpend = (
24
+ data: UsageLog[],
25
+ ): {
26
+ today: number | string;
27
+ yesterday: number | string;
28
+ } => {
29
+ if (!data || data?.length === 0) return { today: 0, yesterday: 0 };
30
+
31
+ const today = data.find((log) => dayjs.utc(log.day).isToday())?.totalSpend ?? 0;
32
+ const yesterday = data.find((log) => dayjs.utc(log.day).isYesterday())?.totalSpend ?? 0;
33
+
34
+ return {
35
+ today: formatNumber(today),
36
+ yesterday: formatNumber(yesterday),
37
+ };
38
+ };
39
+
40
+ const TodaySpend = memo<UsageChartProps>(({ data, isLoading }) => {
41
+ const { t } = useTranslation('auth');
42
+ const theme = useTheme();
43
+
44
+ const { today, yesterday } = computeSpend(data || []);
45
+
46
+ return (
47
+ <StatisticCard
48
+ highlight={theme.green}
49
+ loading={isLoading}
50
+ statistic={{
51
+ description: <Statistic title={t('usage.cards.today.yesterday')} value={yesterday} />,
52
+ precision: 2,
53
+ prefix: '$',
54
+ value: today,
55
+ }}
56
+ title={
57
+ <TitleWithPercentage
58
+ count={typeof today === 'number' ? today : 0}
59
+ prvCount={typeof yesterday === 'number' ? yesterday : 0}
60
+ title={t('usage.cards.today.title')}
61
+ />
62
+ }
63
+ />
64
+ );
65
+ });
66
+
67
+ export default TodaySpend;
@@ -0,0 +1,19 @@
1
+ import { memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import { UsageChartProps } from '../../Client';
5
+ import ActiveModels from './ActiveModels';
6
+ import MonthSpend from './MonthSpend';
7
+ import TodaySpend from './TodaySpend';
8
+
9
+ const UsageCards = memo<UsageChartProps>(({ isLoading, data, groupBy }) => {
10
+ return (
11
+ <Flexbox gap={16} horizontal>
12
+ <TodaySpend data={data} isLoading={isLoading} />
13
+ <MonthSpend data={data} isLoading={isLoading} />
14
+ <ActiveModels data={data} groupBy={groupBy} isLoading={isLoading} />
15
+ </Flexbox>
16
+ );
17
+ });
18
+
19
+ export default UsageCards;
@@ -0,0 +1,145 @@
1
+ import { ProviderIcon } from '@lobehub/icons';
2
+ import { Tag } from '@lobehub/ui';
3
+ import { Table, TableColumnType, Typography } from 'antd';
4
+ import { useTheme } from 'antd-style';
5
+ import { parseAsInteger, useQueryState } from 'nuqs';
6
+ import { memo, useEffect } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { useClientDataSWR } from '@/libs/swr';
11
+ import { usageService } from '@/services/usage';
12
+ import { formatDate, formatNumber } from '@/utils/format';
13
+
14
+ import { UsageChartProps } from '../Client';
15
+
16
+ const UsageTable = memo<UsageChartProps>(({ dateStrings }) => {
17
+ const theme = useTheme();
18
+ const { t } = useTranslation('auth');
19
+
20
+ const { data, isLoading, mutate } = useClientDataSWR('usage-logs', async () =>
21
+ usageService.findByMonth(dateStrings),
22
+ );
23
+
24
+ const [currentPage, setCurrentPage] = useQueryState(
25
+ 'current',
26
+ parseAsInteger.withDefault(1).withOptions({ clearOnDefault: true }),
27
+ );
28
+ const [pageSize, setPageSize] = useQueryState(
29
+ 'pageSize',
30
+ parseAsInteger.withDefault(5).withOptions({ clearOnDefault: true }),
31
+ );
32
+
33
+ useEffect(() => {
34
+ if (dateStrings) {
35
+ mutate();
36
+ }
37
+ }, [dateStrings]);
38
+
39
+ const columns: TableColumnType<any>[] = [
40
+ {
41
+ hidden: true,
42
+ key: 'id',
43
+ title: 'ID',
44
+ },
45
+ {
46
+ dataIndex: 'model',
47
+ key: 'model',
48
+ render: (value, record) => (
49
+ <Flexbox align={'start'} gap={16} horizontal>
50
+ <ProviderIcon
51
+ provider={record.provider}
52
+ size={18}
53
+ style={{
54
+ border: `2px solid ${theme.colorBgContainer}`,
55
+ boxSizing: 'content-box',
56
+ marginRight: -8,
57
+ }}
58
+ />
59
+ <Typography.Text>
60
+ {value?.length > 12 ? `${value.slice(0, 12)}...` : value}
61
+ </Typography.Text>
62
+ </Flexbox>
63
+ ),
64
+ title: t('usage.table.model'),
65
+ },
66
+ {
67
+ dataIndex: 'type',
68
+ filters: [
69
+ {
70
+ text: 'Chat',
71
+ value: 'chat',
72
+ },
73
+ ],
74
+ key: 'type',
75
+ onFilter: (value, record) => record.callType === value,
76
+ render: (value) => {
77
+ return <Tag>{value}</Tag>;
78
+ },
79
+ title: t('usage.table.type'),
80
+ },
81
+ {
82
+ dataIndex: 'totalInputTokens',
83
+ key: 'inputTokens',
84
+ title: t('usage.table.inputTokens'),
85
+ },
86
+ {
87
+ dataIndex: 'totalOutputTokens',
88
+ key: 'outputTokens',
89
+ title: t('usage.table.outputTokens'),
90
+ },
91
+ {
92
+ dataIndex: 'tps',
93
+ key: 'tps',
94
+ render: (value) => formatNumber(value, 2),
95
+ title: t('usage.table.tps'),
96
+ },
97
+ {
98
+ dataIndex: 'ttft',
99
+ key: 'ttft',
100
+ render: (value) => formatNumber(value / 1000, 2),
101
+ title: t('usage.table.ttft'),
102
+ },
103
+ {
104
+ dataIndex: 'spend',
105
+ key: 'spend',
106
+ render: (value) => {
107
+ return `$${formatNumber(value, 6)}`;
108
+ },
109
+ title: t('usage.table.spend'),
110
+ },
111
+ {
112
+ dataIndex: 'createdAt',
113
+ key: 'createdAt',
114
+ render: (value) => {
115
+ return formatDate(new Date(value));
116
+ },
117
+ sortDirections: ['descend'],
118
+ sorter: (a, b) => a.createdAt - b.createdAt,
119
+ title: t('usage.table.createdAt'),
120
+ },
121
+ ];
122
+
123
+ return (
124
+ <Table
125
+ columns={columns}
126
+ dataSource={data}
127
+ key="id"
128
+ loading={isLoading}
129
+ pagination={{
130
+ current: currentPage,
131
+ onChange: (page) => {
132
+ setCurrentPage(page);
133
+ },
134
+ onShowSizeChange: (current, size) => {
135
+ setCurrentPage(current);
136
+ setPageSize(size);
137
+ },
138
+ pageSize,
139
+ }}
140
+ size="small"
141
+ />
142
+ );
143
+ });
144
+
145
+ export default UsageTable;
@@ -0,0 +1,107 @@
1
+ import { type BarChartProps } from '@lobehub/charts';
2
+ import { Segmented } from '@lobehub/ui';
3
+ import { memo, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import StatisticCard from '@/components/StatisticCard';
7
+ import { UsageLog } from '@/types/usage/usageRecord';
8
+ import { formatNumber } from '@/utils/format';
9
+
10
+ import { GroupBy, UsageChartProps } from '../Client';
11
+ import { UsageBarChart } from './components/UsageBarChart';
12
+
13
+ const groupByType = (
14
+ data: UsageLog[],
15
+ type: 'spend' | 'token',
16
+ groupBy: GroupBy,
17
+ ): { categories: string[]; data: BarChartProps['data'] } => {
18
+ if (!data || data?.length === 0) return { categories: [], data: [] };
19
+ let formattedData: BarChartProps['data'] = [];
20
+ let cate: Map<string, number> = data.reduce((acc, log) => {
21
+ if (log.records) {
22
+ for (const item of log.records) {
23
+ if (groupBy === GroupBy.Model && item.model) {
24
+ acc.set(item.model, 0);
25
+ } else if (groupBy === GroupBy.Provider && item.provider) {
26
+ acc.set(item.provider, 0);
27
+ }
28
+ }
29
+ }
30
+ return acc;
31
+ }, new Map<string, number>());
32
+ const categories: string[] = Array.from(cate.keys());
33
+ formattedData = data.map((log) => {
34
+ const totalObj = {
35
+ day: log.day,
36
+ total: type === 'spend' ? log.totalSpend : log.totalTokens,
37
+ };
38
+ let todayCate = new Map<string, number>(cate);
39
+ for (const item of log.records) {
40
+ const value = type === 'spend' ? item.spend || 0 : item.totalTokens || 0;
41
+ const key = groupBy === GroupBy.Model ? item.model : item.provider;
42
+ let displayValue = (todayCate.get(key) || 0) + value;
43
+ if (type === 'spend') {
44
+ const formattedNum = formatNumber((todayCate.get(key) || 0) + value, 2);
45
+ if (typeof formattedNum !== 'string') {
46
+ displayValue = formattedNum;
47
+ }
48
+ }
49
+ todayCate.set(key, displayValue);
50
+ }
51
+ return {
52
+ ...totalObj,
53
+ ...Object.fromEntries(todayCate.entries()),
54
+ };
55
+ });
56
+ return {
57
+ categories,
58
+ data: formattedData,
59
+ };
60
+ };
61
+
62
+ enum ShowType {
63
+ Spend = 'spend',
64
+ Token = 'token',
65
+ }
66
+
67
+ const UsageTrends = memo<UsageChartProps>(({ isLoading, data, groupBy }) => {
68
+ const { t } = useTranslation('auth');
69
+
70
+ const [type, setType] = useState<ShowType>(ShowType.Spend);
71
+
72
+ const { categories: spendCate, data: spendData } = groupByType(
73
+ data || [],
74
+ 'spend',
75
+ groupBy || GroupBy.Model,
76
+ );
77
+ const { categories: tokenCate, data: tokenData } = groupByType(
78
+ data || [],
79
+ 'token',
80
+ groupBy || GroupBy.Model,
81
+ );
82
+ return (
83
+ <StatisticCard
84
+ chart={
85
+ data &&
86
+ (type === ShowType.Spend ? (
87
+ <UsageBarChart categories={spendCate} data={spendData} index="day" />
88
+ ) : (
89
+ <UsageBarChart categories={tokenCate} data={tokenData} index="day" />
90
+ ))
91
+ }
92
+ extra={
93
+ <Segmented
94
+ onChange={(value) => setType(value as ShowType)}
95
+ options={[
96
+ { label: t('usage.trends.spend'), value: ShowType.Spend },
97
+ { label: t('usage.trends.tokens'), value: ShowType.Token },
98
+ ]}
99
+ value={type}
100
+ />
101
+ }
102
+ loading={isLoading}
103
+ />
104
+ );
105
+ });
106
+
107
+ export default UsageTrends;
@@ -0,0 +1,48 @@
1
+ import { Divider, Typography } from 'antd'
2
+ import { Flexbox } from 'react-layout-kit';
3
+ import { BarChart, ChartTooltipFrame, ChartTooltipRow, type BarChartProps } from '@lobehub/charts';
4
+
5
+ export const UsageBarChart = ({ ...props }: BarChartProps) => (
6
+ <BarChart
7
+ {...props}
8
+ customTooltip={({ active, payload, label, valueFormatter }) => {
9
+ if (active && payload) {
10
+ return (
11
+ <ChartTooltipFrame>
12
+ <Flexbox
13
+ horizontal
14
+ justify={'space-between'}
15
+ paddingBlock={8}
16
+ paddingInline={16}
17
+ >
18
+ <Typography.Paragraph ellipsis style={{ margin: 0 }}>
19
+ {label}
20
+ </Typography.Paragraph>
21
+ <span style={{ fontWeight: 'bold' }}>
22
+ {payload.reduce((acc: number, cur: any) => acc + cur.value, 0)}
23
+ </span>
24
+ </Flexbox>
25
+ <Divider style={{ margin: 0 }} />
26
+ <Flexbox
27
+ gap={4}
28
+ paddingBlock={8}
29
+ paddingInline={16}
30
+ style={{ flexDirection: 'column-reverse', marginTop: 4 }}
31
+ >
32
+ {payload.map(({ value, color, name }: any, idx: number) => (
33
+ typeof value === 'number' && value > 0 ?
34
+ <ChartTooltipRow
35
+ color={color}
36
+ key={`id-${idx}`}
37
+ name={name}
38
+ value={(valueFormatter as any)?.(value)}
39
+ /> : null
40
+ ))}
41
+ </Flexbox>
42
+ </ChartTooltipFrame>
43
+ );
44
+ }
45
+ return null;
46
+ }}
47
+ />
48
+ )
@@ -0,0 +1,23 @@
1
+ import { metadataModule } from '@/server/metadata';
2
+ import { translation } from '@/server/translation';
3
+ import { DynamicLayoutProps } from '@/types/next';
4
+ import { RouteVariants } from '@/utils/server/routeVariants';
5
+
6
+ import Client from './Client';
7
+
8
+ export const generateMetadata = async (props: DynamicLayoutProps) => {
9
+ const locale = await RouteVariants.getLocale(props);
10
+ const { t } = await translation('auth', locale);
11
+ return metadataModule.generate({
12
+ description: t('header.desc'),
13
+ title: t('tab.usage'),
14
+ url: '/profile/usage',
15
+ });
16
+ };
17
+
18
+ const Page = async (props: DynamicLayoutProps) => {
19
+ const mobile = await RouteVariants.getIsMobile(props);
20
+ return <Client mobile={mobile} />;
21
+ };
22
+
23
+ export default Page;
@@ -46,8 +46,8 @@ const CustomRender = memo<CustomRenderProps>(
46
46
  const [loading] = useChatStore((s) => [messageStateSelectors.isPluginApiInvoking(id)(s)]);
47
47
  const [isEditing, setIsEditing] = useState(false);
48
48
  const { message } = App.useApp();
49
- const [updatePluginArguments, reInvokeToolMessage] = useChatStore((s) => [
50
- s.updatePluginArguments,
49
+ const [optimisticUpdatePluginArguments, reInvokeToolMessage] = useChatStore((s) => [
50
+ s.optimisticUpdatePluginArguments,
51
51
  s.reInvokeToolMessage,
52
52
  ]);
53
53
  const handleCancel = useCallback(() => {
@@ -62,7 +62,7 @@ const CustomRender = memo<CustomRenderProps>(
62
62
  const newArgsString = JSON.stringify(editedObject, null, 2);
63
63
 
64
64
  if (newArgsString !== requestArgs) {
65
- await updatePluginArguments(id, editedObject, true);
65
+ await optimisticUpdatePluginArguments(id, editedObject, true);
66
66
  await reInvokeToolMessage(id);
67
67
  }
68
68
  setIsEditing(false);