@lobehub/chat 0.154.7 → 0.155.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.155.0](https://github.com/lobehub/lobe-chat/compare/v0.154.7...v0.155.0)
6
+
7
+ <sup>Released on **2024-05-07**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add DataStatistics.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Add DataStatistics ([cf474bb](https://github.com/lobehub/lobe-chat/commit/cf474bb))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 0.154.7](https://github.com/lobehub/lobe-chat/compare/v0.154.6...v0.154.7)
6
31
 
7
32
  <sup>Released on **2024-05-07**</sup>
@@ -12,6 +12,12 @@
12
12
  "copy": "نسخ",
13
13
  "copyFail": "فشل في النسخ",
14
14
  "copySuccess": "تم النسخ بنجاح",
15
+ "dataStatistics": {
16
+ "messages": "رسائل",
17
+ "sessions": "جلسات",
18
+ "today": "اليوم",
19
+ "topics": "مواضيع"
20
+ },
15
21
  "defaultAgent": "مساعد افتراضي",
16
22
  "defaultSession": "جلسة افتراضية",
17
23
  "delete": "حذف",
@@ -12,6 +12,12 @@
12
12
  "copy": "Копирай",
13
13
  "copyFail": "Копирането не е успешно",
14
14
  "copySuccess": "Копирано успешно",
15
+ "dataStatistics": {
16
+ "messages": "Съобщения",
17
+ "sessions": "Сесии",
18
+ "today": "Днес",
19
+ "topics": "Теми"
20
+ },
15
21
  "defaultAgent": "Агент по подразбиране",
16
22
  "defaultSession": "Агент по подразбиране",
17
23
  "delete": "Изтрий",
@@ -12,6 +12,12 @@
12
12
  "copy": "Kopieren",
13
13
  "copyFail": "Kopieren fehlgeschlagen",
14
14
  "copySuccess": "Kopieren erfolgreich",
15
+ "dataStatistics": {
16
+ "messages": "Nachrichten",
17
+ "sessions": "Sitzungen",
18
+ "today": "Heute",
19
+ "topics": "Themen"
20
+ },
15
21
  "defaultAgent": "Standardassistent",
16
22
  "defaultSession": "Standardassistent",
17
23
  "delete": "Löschen",
@@ -12,6 +12,12 @@
12
12
  "copy": "Copy",
13
13
  "copyFail": "Copy failed",
14
14
  "copySuccess": "Copied successfully",
15
+ "dataStatistics": {
16
+ "messages": "Messages",
17
+ "sessions": "Assistants",
18
+ "today": "Today's New",
19
+ "topics": "Topics"
20
+ },
15
21
  "defaultAgent": "Default Agent",
16
22
  "defaultSession": "Default Agent",
17
23
  "delete": "Delete",
@@ -12,6 +12,12 @@
12
12
  "copy": "Copiar",
13
13
  "copyFail": "Fallo al copiar",
14
14
  "copySuccess": "¡Copia exitosa!",
15
+ "dataStatistics": {
16
+ "messages": "Mensajes",
17
+ "sessions": "Sesiones",
18
+ "today": "Hoy",
19
+ "topics": "Temas"
20
+ },
15
21
  "defaultAgent": "Asistente predeterminado",
16
22
  "defaultSession": "Sesión predeterminada",
17
23
  "delete": "Eliminar",
@@ -12,6 +12,12 @@
12
12
  "copy": "Copier",
13
13
  "copyFail": "Échec de la copie",
14
14
  "copySuccess": "Copie réussie",
15
+ "dataStatistics": {
16
+ "messages": "Messages",
17
+ "sessions": "Sessions",
18
+ "today": "Aujourd'hui",
19
+ "topics": "Sujets"
20
+ },
15
21
  "defaultAgent": "Agent par défaut",
16
22
  "defaultSession": "Session par défaut",
17
23
  "delete": "Supprimer",
@@ -12,6 +12,12 @@
12
12
  "copy": "Copia",
13
13
  "copyFail": "Copia non riuscita",
14
14
  "copySuccess": "Copia riuscita",
15
+ "dataStatistics": {
16
+ "messages": "Messaggi",
17
+ "sessions": "Sessioni",
18
+ "today": "Oggi",
19
+ "topics": "Argomenti"
20
+ },
15
21
  "defaultAgent": "Assistente predefinito",
16
22
  "defaultSession": "Sessione predefinita",
17
23
  "delete": "Elimina",
@@ -12,6 +12,12 @@
12
12
  "copy": "コピー",
13
13
  "copyFail": "コピーに失敗しました",
14
14
  "copySuccess": "コピーが成功しました",
15
+ "dataStatistics": {
16
+ "messages": "メッセージ",
17
+ "sessions": "セッション",
18
+ "today": "今日の追加",
19
+ "topics": "トピック"
20
+ },
15
21
  "defaultAgent": "デフォルトエージェント",
16
22
  "defaultSession": "デフォルトセッション",
17
23
  "delete": "削除",
@@ -12,6 +12,12 @@
12
12
  "copy": "복사",
13
13
  "copyFail": "복사 실패",
14
14
  "copySuccess": "복사 성공",
15
+ "dataStatistics": {
16
+ "messages": "메시지",
17
+ "sessions": "세션",
18
+ "today": "오늘",
19
+ "topics": "주제"
20
+ },
15
21
  "defaultAgent": "기본 에이전트",
16
22
  "defaultSession": "기본 세션",
17
23
  "delete": "삭제",
@@ -12,6 +12,12 @@
12
12
  "copy": "Kopiëren",
13
13
  "copyFail": "Kopiëren mislukt",
14
14
  "copySuccess": "Kopiëren gelukt",
15
+ "dataStatistics": {
16
+ "messages": "Berichten",
17
+ "sessions": "Sessies",
18
+ "today": "Vandaag",
19
+ "topics": "Onderwerpen"
20
+ },
15
21
  "defaultAgent": "Standaard assistent",
16
22
  "defaultSession": "Standaard assistent",
17
23
  "delete": "Verwijderen",
@@ -12,6 +12,12 @@
12
12
  "copy": "Kopiuj",
13
13
  "copyFail": "Nie udało się skopiować",
14
14
  "copySuccess": "Skopiowano pomyślnie",
15
+ "dataStatistics": {
16
+ "messages": "Wiadomości",
17
+ "sessions": "Sesje",
18
+ "today": "Dzisiaj",
19
+ "topics": "Tematy"
20
+ },
15
21
  "defaultAgent": "Domyślny asystent",
16
22
  "defaultSession": "Domyślna sesja",
17
23
  "delete": "Usuń",
@@ -12,6 +12,12 @@
12
12
  "copy": "Copiar",
13
13
  "copyFail": "Falha ao copiar",
14
14
  "copySuccess": "Cópia bem-sucedida",
15
+ "dataStatistics": {
16
+ "messages": "Mensagens",
17
+ "sessions": "Sessões",
18
+ "today": "Hoje",
19
+ "topics": "Tópicos"
20
+ },
15
21
  "defaultAgent": "Assistente padrão",
16
22
  "defaultSession": "Sessão padrão",
17
23
  "delete": "Excluir",
@@ -12,6 +12,12 @@
12
12
  "copy": "Копировать",
13
13
  "copyFail": "Не удалось скопировать",
14
14
  "copySuccess": "Успешно скопировано",
15
+ "dataStatistics": {
16
+ "messages": "Сообщения",
17
+ "sessions": "Сессии",
18
+ "today": "Сегодня",
19
+ "topics": "Темы"
20
+ },
15
21
  "defaultAgent": "Пользовательский агент",
16
22
  "defaultSession": "Пользовательский агент",
17
23
  "delete": "Удалить",
@@ -12,6 +12,12 @@
12
12
  "copy": "Kopyala",
13
13
  "copyFail": "Kopyalama başarısız oldu",
14
14
  "copySuccess": "Kopyalama Başarılı",
15
+ "dataStatistics": {
16
+ "messages": "Mesajlar",
17
+ "sessions": "Oturumlar",
18
+ "today": "Bugün",
19
+ "topics": "Konular"
20
+ },
15
21
  "defaultAgent": "Varsayılan Asistan",
16
22
  "defaultSession": "Varsayılan Asistan",
17
23
  "delete": "Sil",
@@ -12,6 +12,12 @@
12
12
  "copy": "Sao chép",
13
13
  "copyFail": "Sao chép thất bại",
14
14
  "copySuccess": "Sao chép thành công",
15
+ "dataStatistics": {
16
+ "messages": "Tin nhắn",
17
+ "sessions": "Phiên làm việc",
18
+ "today": "Hôm nay",
19
+ "topics": "Chủ đề"
20
+ },
15
21
  "defaultAgent": "Trợ lý mặc định",
16
22
  "defaultSession": "Phiên mặc định",
17
23
  "delete": "Xóa",
@@ -12,6 +12,12 @@
12
12
  "copy": "复制",
13
13
  "copyFail": "复制失败",
14
14
  "copySuccess": "复制成功",
15
+ "dataStatistics": {
16
+ "messages": "消息",
17
+ "sessions": "助手",
18
+ "today": "今日新增",
19
+ "topics": "话题"
20
+ },
15
21
  "defaultAgent": "自定义助手",
16
22
  "defaultSession": "自定义助手",
17
23
  "delete": "删除",
@@ -12,6 +12,12 @@
12
12
  "copy": "複製",
13
13
  "copyFail": "複製失敗",
14
14
  "copySuccess": "複製成功",
15
+ "dataStatistics": {
16
+ "messages": "消息",
17
+ "sessions": "助手",
18
+ "today": "今日新增",
19
+ "topics": "話題"
20
+ },
15
21
  "defaultAgent": "預設助手",
16
22
  "defaultSession": "預設助手",
17
23
  "delete": "刪除",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.154.7",
3
+ "version": "0.155.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -4,13 +4,12 @@ import { PropsWithChildren, memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import UserAvatar from '@/features/User/UserAvatar';
7
- import UserInfo from '@/features/User/UserInfo';
8
7
 
9
8
  import { useStyles } from './style';
10
9
 
11
10
  export const AVATAR_SIZE = 80;
12
11
 
13
- const AvatarBanner = memo<PropsWithChildren>(() => {
12
+ const AvatarBanner = memo<PropsWithChildren>(({ children }) => {
14
13
  const { styles } = useStyles();
15
14
 
16
15
  return (
@@ -20,7 +19,7 @@ const AvatarBanner = memo<PropsWithChildren>(() => {
20
19
  <UserAvatar shape={'square'} size={AVATAR_SIZE} />
21
20
  </div>
22
21
  </Flexbox>
23
- <UserInfo className={styles.info} />
22
+ <Flexbox className={styles.info}>{children}</Flexbox>
24
23
  </>
25
24
  );
26
25
  });
@@ -4,6 +4,7 @@ import { Skeleton } from 'antd';
4
4
  import { memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
+ import Divider from '@/components/Cell/Divider';
7
8
  import SkeletonLoading from '@/components/SkeletonLoading';
8
9
 
9
10
  import { useStyles } from './features/style';
@@ -22,9 +23,25 @@ const Loading = memo(() => {
22
23
  paddingInline={12}
23
24
  >
24
25
  <Skeleton.Avatar active shape={'circle'} size={48} />
25
- <Skeleton active paragraph={{ rows: 1 }} title={false} />
26
+ <Skeleton.Button active block />
26
27
  </Flexbox>
27
- <SkeletonLoading active paragraph={{ rows: 8 }} title={false} />
28
+ <Flexbox gap={4} horizontal paddingBlock={12} paddingInline={16}>
29
+ <Skeleton.Button active block />
30
+ <Skeleton.Button active block />
31
+ <Skeleton.Button active block />
32
+ </Flexbox>
33
+ <Divider />
34
+ <SkeletonLoading
35
+ active
36
+ paragraph={{ rows: 6, style: { marginBottom: 0 }, width: '100%' }}
37
+ title={false}
38
+ />
39
+ <Divider />
40
+ <SkeletonLoading
41
+ active
42
+ paragraph={{ rows: 3, style: { marginBottom: 0 }, width: '100%' }}
43
+ title={false}
44
+ />
28
45
  </>
29
46
  );
30
47
  });
@@ -3,6 +3,8 @@ import { Center } from 'react-layout-kit';
3
3
 
4
4
  import BrandWatermark from '@/components/BrandWatermark';
5
5
  import Divider from '@/components/Cell/Divider';
6
+ import DataStatistics from '@/features/User/DataStatistics';
7
+ import UserInfo from '@/features/User/UserInfo';
6
8
  import { isMobileDevice } from '@/utils/responsive';
7
9
 
8
10
  import AvatarBanner from './features/AvatarBanner';
@@ -16,7 +18,10 @@ const Page = () => {
16
18
 
17
19
  return (
18
20
  <>
19
- <AvatarBanner />
21
+ <AvatarBanner>
22
+ <UserInfo />
23
+ <DataStatistics paddingInline={16} />
24
+ </AvatarBanner>
20
25
  <Divider />
21
26
  <Cate />
22
27
  <ExtraCate />
@@ -118,6 +118,10 @@ class _TopicModel extends BaseModel {
118
118
  return this.table.get(id);
119
119
  }
120
120
 
121
+ async count() {
122
+ return this.table.count();
123
+ }
124
+
121
125
  // **************** Create *************** //
122
126
 
123
127
  async create({ title, favorite, sessionId, messages }: CreateTopicParams, id = nanoid()) {
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+
3
+ import { Icon, Tooltip } from '@lobehub/ui';
4
+ import { Badge } from 'antd';
5
+ import { createStyles } from 'antd-style';
6
+ import { isNumber } from 'lodash-es';
7
+ import { LoaderCircle } from 'lucide-react';
8
+ import { memo, useMemo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Flexbox, FlexboxProps } from 'react-layout-kit';
11
+ import useSWR from 'swr';
12
+
13
+ import { messageService } from '@/services/message';
14
+ import { sessionService } from '@/services/session';
15
+ import { topicService } from '@/services/topic';
16
+
17
+ const useStyles = createStyles(({ css, token }) => ({
18
+ card: css`
19
+ padding: 6px 8px;
20
+ background: ${token.colorFillTertiary};
21
+ border-radius: ${token.borderRadius}px;
22
+ `,
23
+ count: css`
24
+ font-size: 16px;
25
+ font-weight: bold;
26
+ line-height: 1.2;
27
+ `,
28
+ title: css`
29
+ font-size: 12px;
30
+ line-height: 1.2;
31
+ color: ${token.colorTextDescription};
32
+ `,
33
+ today: css`
34
+ font-size: 12px;
35
+ `,
36
+ }));
37
+
38
+ const formatNumber = (num: any) => {
39
+ if (!isNumber(num)) return num;
40
+ // 使用Intl.NumberFormat来添加千分号
41
+ const formattedWithComma = new Intl.NumberFormat('en-US').format(num);
42
+
43
+ // 格式化为 K 或 M
44
+ if (num >= 10_000_000) {
45
+ return (num / 1_000_000).toFixed(1) + 'M';
46
+ } else if (num >= 10_000) {
47
+ return (num / 1000).toFixed(1) + 'K';
48
+ } else if (num === 0) {
49
+ return 0;
50
+ } else {
51
+ return formattedWithComma;
52
+ }
53
+ };
54
+
55
+ const DataStatistics = memo<Omit<FlexboxProps, 'children'>>(({ style, ...rest }) => {
56
+ // sessions
57
+ const { data: sessions, isLoading: sessionsLoading } = useSWR(
58
+ 'count-sessions',
59
+ sessionService.countSessions,
60
+ );
61
+ // topics
62
+ const { data: topics, isLoading: topicsLoading } = useSWR(
63
+ 'count-topics',
64
+ topicService.countTopics,
65
+ );
66
+ // messages
67
+ const { data: messages, isLoading: messagesLoading } = useSWR(
68
+ 'count-messages',
69
+ messageService.countMessages,
70
+ );
71
+ const { data: messagesToday } = useSWR('today-messages', messageService.countTodayMessages);
72
+
73
+ const { styles, theme } = useStyles();
74
+ const { t } = useTranslation('common');
75
+
76
+ const loading = useMemo(() => <Icon icon={LoaderCircle} spin />, []);
77
+
78
+ const items = [
79
+ {
80
+ count: sessionsLoading ? loading : sessions,
81
+ key: 'sessions',
82
+ title: t('dataStatistics.sessions'),
83
+ },
84
+ {
85
+ count: topicsLoading ? loading : topics,
86
+ key: 'topics',
87
+ title: t('dataStatistics.topics'),
88
+ },
89
+ {
90
+ count: messagesLoading ? loading : messages,
91
+ countToady: messagesToday,
92
+ key: 'messages',
93
+ title: t('dataStatistics.messages'),
94
+ },
95
+ ];
96
+
97
+ return (
98
+ <Flexbox
99
+ align={'center'}
100
+ gap={4}
101
+ horizontal
102
+ paddingInline={8}
103
+ style={{ marginBottom: 8, ...style }}
104
+ width={'100%'}
105
+ {...rest}
106
+ >
107
+ {items.map((item) => {
108
+ if (item.key === 'messages') {
109
+ const showBadge = Boolean(item.countToady && item.countToady > 0);
110
+ return (
111
+ <Flexbox
112
+ align={'center'}
113
+ className={styles.card}
114
+ flex={showBadge ? 2 : 1}
115
+ gap={4}
116
+ horizontal
117
+ justify={'space-between'}
118
+ key={item.key}
119
+ >
120
+ <Flexbox gap={2}>
121
+ <div className={styles.count}>{formatNumber(item.count)}</div>
122
+ <div className={styles.title}>{item.title}</div>
123
+ </Flexbox>
124
+ {showBadge && (
125
+ <Tooltip title={t('dataStatistics.today')}>
126
+ <Badge
127
+ count={`+${item.countToady}`}
128
+ style={{
129
+ background: theme.colorSuccess,
130
+ color: theme.colorSuccessBg,
131
+ cursor: 'pointer',
132
+ }}
133
+ />
134
+ </Tooltip>
135
+ )}
136
+ </Flexbox>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <Flexbox className={styles.card} flex={1} gap={2} key={item.key}>
142
+ <Flexbox horizontal>
143
+ <div className={styles.count}>{formatNumber(item.count)}</div>
144
+ </Flexbox>
145
+ <div className={styles.title}>{item.title}</div>
146
+ </Flexbox>
147
+ );
148
+ })}
149
+ </Flexbox>
150
+ );
151
+ });
152
+
153
+ export default DataStatistics;
@@ -8,6 +8,7 @@ import { enableAuth } from '@/const/auth';
8
8
  import { useUserStore } from '@/store/user';
9
9
  import { authSelectors } from '@/store/user/selectors';
10
10
 
11
+ import DataStatistics from '../DataStatistics';
11
12
  import UserInfo from '../UserInfo';
12
13
  import UserLoginOrSignup from '../UserLoginOrSignup';
13
14
  import LangButton from './LangButton';
@@ -50,6 +51,7 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
50
51
  ) : (
51
52
  <UserLoginOrSignup onClick={handleSignIn} />
52
53
  )}
54
+ <DataStatistics />
53
55
  <Menu items={mainItems} onClick={closePopover} />
54
56
  <Flexbox
55
57
  align={'center'}
@@ -11,8 +11,13 @@ export default {
11
11
  close: '关闭',
12
12
  copy: '复制',
13
13
  copyFail: '复制失败',
14
-
15
14
  copySuccess: '复制成功',
15
+ dataStatistics: {
16
+ messages: '消息',
17
+ sessions: '助手',
18
+ today: '今日新增',
19
+ topics: '话题',
20
+ },
16
21
  defaultAgent: '自定义助手',
17
22
  defaultSession: '自定义助手',
18
23
  delete: '删除',
@@ -1,3 +1,5 @@
1
+ import dayjs from 'dayjs';
2
+
1
3
  import { MessageModel } from '@/database/client/models/message';
2
4
  import { DB_Message } from '@/database/client/schemas/message';
3
5
  import { ChatMessage, ChatMessageError, ChatPluginPayload } from '@/types/message';
@@ -27,6 +29,13 @@ export class ClientService implements IMessageService {
27
29
  return MessageModel.count();
28
30
  }
29
31
 
32
+ async countTodayMessages() {
33
+ const topics = await MessageModel.queryAll();
34
+ return topics.filter(
35
+ (item) => dayjs(item.createdAt).format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD'),
36
+ ).length;
37
+ }
38
+
30
39
  async getAllMessagesInSession(sessionId: string) {
31
40
  return MessageModel.queryBySessionId(sessionId);
32
41
  }
@@ -79,6 +79,7 @@ export class ClientService implements ISessionService {
79
79
  async countSessions() {
80
80
  return SessionModel.count();
81
81
  }
82
+
82
83
  async hasSessions() {
83
84
  return (await this.countSessions()) !== 0;
84
85
  }
@@ -1,5 +1,6 @@
1
1
  import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
+ import { SessionModel } from '@/database/client/models/session';
3
4
  import { CreateTopicParams, TopicModel } from '@/database/client/models/topic';
4
5
  import { ChatTopic } from '@/types/topic';
5
6
 
@@ -13,6 +14,7 @@ vi.mock('@/database/client/models/topic', () => {
13
14
  create: vi.fn(),
14
15
  query: vi.fn(),
15
16
  delete: vi.fn(),
17
+ count: vi.fn(),
16
18
  batchDeleteBySessionId: vi.fn(),
17
19
  batchDelete: vi.fn(),
18
20
  clearTable: vi.fn(),
@@ -214,4 +216,30 @@ describe('TopicService', () => {
214
216
  expect(result).toBe(mockTopics);
215
217
  });
216
218
  });
219
+
220
+ describe('countTopics', () => {
221
+ it('should return false if no topics exist', async () => {
222
+ // Setup
223
+ (TopicModel.count as Mock).mockResolvedValue(0);
224
+
225
+ // Execute
226
+ const result = await topicService.countTopics();
227
+
228
+ // Assert
229
+ expect(TopicModel.count).toHaveBeenCalled();
230
+ expect(result).toBe(0);
231
+ });
232
+
233
+ it('should return true if topics exist', async () => {
234
+ // Setup
235
+ (TopicModel.count as Mock).mockResolvedValue(1);
236
+
237
+ // Execute
238
+ const result = await topicService.countTopics();
239
+
240
+ // Assert
241
+ expect(TopicModel.count).toHaveBeenCalled();
242
+ expect(result).toBe(1);
243
+ });
244
+ });
217
245
  });
@@ -34,6 +34,10 @@ export class ClientService implements ITopicService {
34
34
  return TopicModel.queryAll();
35
35
  }
36
36
 
37
+ async countTopics() {
38
+ return TopicModel.count();
39
+ }
40
+
37
41
  async updateTopicFavorite(id: string, favorite?: boolean) {
38
42
  return this.updateTopic(id, { favorite });
39
43
  }