@lobehub/lobehub 2.0.0-next.37 → 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 (127) hide show
  1. package/CHANGELOG.md +50 -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 +14 -0
  7. package/locales/ar/auth.json +45 -1
  8. package/locales/ar/modelProvider.json +13 -1
  9. package/locales/bg-BG/auth.json +45 -1
  10. package/locales/bg-BG/modelProvider.json +13 -1
  11. package/locales/de-DE/auth.json +45 -1
  12. package/locales/de-DE/modelProvider.json +13 -1
  13. package/locales/en-US/auth.json +45 -1
  14. package/locales/en-US/modelProvider.json +13 -1
  15. package/locales/es-ES/auth.json +45 -1
  16. package/locales/es-ES/modelProvider.json +13 -1
  17. package/locales/fa-IR/auth.json +45 -1
  18. package/locales/fa-IR/modelProvider.json +13 -1
  19. package/locales/fr-FR/auth.json +45 -1
  20. package/locales/fr-FR/modelProvider.json +13 -1
  21. package/locales/it-IT/auth.json +45 -1
  22. package/locales/it-IT/modelProvider.json +13 -1
  23. package/locales/ja-JP/auth.json +45 -1
  24. package/locales/ja-JP/modelProvider.json +13 -1
  25. package/locales/ko-KR/auth.json +45 -1
  26. package/locales/ko-KR/modelProvider.json +13 -1
  27. package/locales/nl-NL/auth.json +45 -1
  28. package/locales/nl-NL/modelProvider.json +13 -1
  29. package/locales/pl-PL/auth.json +45 -1
  30. package/locales/pl-PL/modelProvider.json +13 -1
  31. package/locales/pt-BR/auth.json +45 -1
  32. package/locales/pt-BR/modelProvider.json +13 -1
  33. package/locales/ru-RU/auth.json +45 -1
  34. package/locales/ru-RU/modelProvider.json +13 -1
  35. package/locales/tr-TR/auth.json +45 -1
  36. package/locales/tr-TR/modelProvider.json +13 -1
  37. package/locales/vi-VN/auth.json +45 -1
  38. package/locales/vi-VN/modelProvider.json +13 -1
  39. package/locales/zh-CN/auth.json +45 -1
  40. package/locales/zh-CN/modelProvider.json +13 -1
  41. package/locales/zh-TW/auth.json +45 -1
  42. package/locales/zh-TW/modelProvider.json +13 -1
  43. package/package.json +1 -1
  44. package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
  46. package/packages/obervability-otel/package.json +3 -1
  47. package/packages/obervability-otel/src/api.ts +2 -0
  48. package/packages/obervability-otel/src/trpc/convention.ts +16 -0
  49. package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
  50. package/packages/obervability-otel/src/trpc/index.ts +62 -0
  51. package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
  52. package/packages/types/src/usage/usageRecord.ts +54 -0
  53. package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
  54. package/packages/web-crawler/src/crawImpl/naive.ts +9 -9
  55. package/packages/web-crawler/src/crawler.ts +5 -5
  56. package/packages/web-crawler/src/urlRules.ts +13 -13
  57. package/packages/web-crawler/src/utils/appUrlRules.ts +5 -5
  58. package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
  59. package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
  60. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
  61. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
  62. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
  63. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
  64. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
  65. package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
  66. package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
  67. package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
  68. package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
  69. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
  70. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
  71. package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
  72. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
  73. package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
  74. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
  75. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
  76. package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
  77. package/src/features/Conversation/Messages/Group/index.tsx +7 -2
  78. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +3 -0
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
  80. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  81. package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
  82. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
  83. package/src/features/PluginsUI/Render/index.tsx +17 -0
  84. package/src/libs/mcp/client.ts +3 -2
  85. package/src/libs/mcp/types.ts +71 -0
  86. package/src/libs/trpc/lambda/index.ts +5 -2
  87. package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
  88. package/src/locales/default/auth.ts +44 -0
  89. package/src/locales/default/chat.ts +1 -0
  90. package/src/server/routers/desktop/mcp.ts +1 -3
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/usage.ts +36 -0
  93. package/src/server/routers/tools/mcp.ts +1 -3
  94. package/src/server/services/mcp/index.test.ts +28 -15
  95. package/src/server/services/mcp/index.ts +29 -18
  96. package/src/server/services/usage/index.test.ts +310 -0
  97. package/src/server/services/usage/index.ts +164 -0
  98. package/src/services/chat/contextEngineering.test.ts +4 -0
  99. package/src/services/mcp.test.ts +7 -1
  100. package/src/services/mcp.ts +13 -12
  101. package/src/services/usage.ts +13 -0
  102. package/src/store/chat/agents/createAgentExecutors.ts +2 -3
  103. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
  104. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
  105. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
  106. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
  107. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
  108. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  109. package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
  110. package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
  111. package/src/store/chat/slices/message/initialState.ts +5 -0
  112. package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
  113. package/src/store/chat/slices/message/selectors/chat.ts +0 -2
  114. package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
  115. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
  116. package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
  117. package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
  118. package/src/store/chat/slices/plugin/action.test.ts +4 -4
  119. package/src/store/chat/slices/plugin/actions/index.ts +39 -0
  120. package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
  121. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
  122. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
  123. package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
  124. package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
  125. package/src/store/chat/store.ts +1 -1
  126. package/src/store/global/initialState.ts +1 -0
  127. package/src/store/chat/slices/plugin/action.ts +0 -539
@@ -0,0 +1,175 @@
1
+ import { CategoryBar, useThemeColorRange } from '@lobehub/charts';
2
+ import { ModelIcon, ProviderIcon } from '@lobehub/icons';
3
+ import { Collapse, Tag } from '@lobehub/ui';
4
+ import { Skeleton } from 'antd';
5
+ import { useTheme } from 'antd-style';
6
+ import { memo, useMemo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import InlineTable from '@/components/InlineTable';
11
+ import { UsageLog } from '@/types/usage/usageRecord';
12
+ import { formatPrice } from '@/utils/format';
13
+
14
+ import { GroupBy, UsageChartProps } from '../../../Client';
15
+
16
+ interface WeightGroup {
17
+ id: string;
18
+ spend: number | string;
19
+ weight: number;
20
+ }
21
+
22
+ const formatData = (
23
+ data: UsageLog[],
24
+ groupBy: GroupBy,
25
+ ): {
26
+ childrens: WeightGroup[];
27
+ id: string;
28
+ totalSpend: number;
29
+ }[] => {
30
+ if (!data || data?.length === 0) return [];
31
+
32
+ const requestLogs = data.flatMap((log) => log.records);
33
+ const groupedLogs = requestLogs.reduce((acc, log) => {
34
+ const key = groupBy === GroupBy.Model ? log.model : log.provider;
35
+ if (!acc.has(key)) {
36
+ acc.set(key, []);
37
+ }
38
+ acc.get(key)?.push(log);
39
+ return acc;
40
+ }, new Map<string, UsageLog['records']>());
41
+
42
+ return Array.from(groupedLogs.entries())
43
+ .map(([key, logs]) => {
44
+ // 此处的 logs 为多日的 log,需要进行 sum
45
+ // 如果当前的 groupBy 是 Model,则 logs 应该按照 Provider 进行分组
46
+ const spend = logs.reduce((acc, log) => {
47
+ const key = groupBy === GroupBy.Model ? log.provider : log.model;
48
+ acc.set(key, (acc.get(key) || 0) + log.spend);
49
+ return acc;
50
+ }, new Map<string, number>());
51
+
52
+ const totalSpend = logs.reduce((total, log) => total + (log.spend || 0), 0);
53
+
54
+ const spendWithWeight = Array.from(
55
+ spend.entries().map(([key, value]) => {
56
+ return {
57
+ id: key,
58
+ spend: value,
59
+ weight: totalSpend > 0 ? value / totalSpend : 0,
60
+ };
61
+ }),
62
+ );
63
+
64
+ return {
65
+ childrens: spendWithWeight.sort((a, b) => b.weight - a.weight),
66
+ id: key,
67
+ totalSpend: totalSpend,
68
+ };
69
+ })
70
+ .sort((a, b) => b.totalSpend - a.totalSpend);
71
+ };
72
+
73
+ const ModelTable = memo<UsageChartProps>(({ data, isLoading, groupBy }) => {
74
+ const { t } = useTranslation('auth');
75
+ const theme = useTheme();
76
+ const themeColorRange = useThemeColorRange();
77
+
78
+ const formattedData = useMemo(
79
+ () => formatData(data || [], groupBy || GroupBy.Model),
80
+ [data, groupBy],
81
+ );
82
+
83
+ console.log('ModelTable', groupBy, formattedData);
84
+
85
+ return isLoading ? (
86
+ <Skeleton active paragraph={{ rows: 8 }} title={false} />
87
+ ) : (
88
+ <Collapse
89
+ defaultActiveKey={formattedData.map((item) => item.id)}
90
+ expandIconPosition={'end'}
91
+ gap={16}
92
+ items={formattedData.map((item) => {
93
+ const key = item.id;
94
+ return {
95
+ children: (
96
+ <Flexbox>
97
+ <CategoryBar
98
+ colors={themeColorRange}
99
+ showLabels={false}
100
+ size={2}
101
+ values={item.childrens.map((item) => item.weight)}
102
+ />
103
+ <InlineTable
104
+ columns={[
105
+ {
106
+ dataIndex: 'id',
107
+ key: 'id',
108
+ render: (value, record, index) => {
109
+ return (
110
+ <Flexbox align={'center'} gap={12} horizontal key={value}>
111
+ {groupBy === GroupBy.Provider ? (
112
+ <ProviderIcon
113
+ provider={record.id}
114
+ style={{
115
+ boxShadow: `0 0 0 2px ${theme.colorBgContainer}, 0 0 0 4px ${themeColorRange[index]}`,
116
+ boxSizing: 'content-box',
117
+ }}
118
+ />
119
+ ) : (
120
+ <ModelIcon
121
+ model={record.id}
122
+ style={{
123
+ boxShadow: `0 0 0 2px ${theme.colorBgContainer}, 0 0 0 4px ${themeColorRange[index]}`,
124
+ boxSizing: 'content-box',
125
+ }}
126
+ />
127
+ )}
128
+ {value}
129
+ </Flexbox>
130
+ );
131
+ },
132
+ title:
133
+ groupBy === GroupBy.Model
134
+ ? t('usage.activeModels.table.provider')
135
+ : t('usage.activeModels.table.model'),
136
+ width: 200,
137
+ },
138
+ {
139
+ dataIndex: 'spend',
140
+ key: 'spend',
141
+ render: (value) => {
142
+ return `$${formatPrice(value)}`;
143
+ },
144
+ title: t('usage.activeModels.table.spend'),
145
+ },
146
+ ]}
147
+ dataSource={item.childrens}
148
+ hoverToActive={false}
149
+ loading={isLoading}
150
+ rowKey={(record) => record.id}
151
+ />
152
+ </Flexbox>
153
+ ),
154
+ extra: <Tag>{item?.childrens?.length ?? 0}</Tag>,
155
+ key,
156
+ label: (
157
+ <Flexbox align={'center'} gap={8} horizontal>
158
+ {groupBy === GroupBy.Model ? (
159
+ <ModelIcon model={key} size={24} />
160
+ ) : (
161
+ <ProviderIcon provider={key} size={24} />
162
+ )}
163
+ {key}
164
+ </Flexbox>
165
+ ),
166
+ };
167
+ })}
168
+ padding={{
169
+ body: 0,
170
+ }}
171
+ />
172
+ );
173
+ });
174
+
175
+ export default ModelTable;
@@ -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;