@lobehub/chat 1.13.0 → 1.13.2

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,57 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.13.2](https://github.com/lobehub/lobe-chat/compare/v1.13.1...v1.13.2)
6
+
7
+ <sup>Released on **2024-08-27**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Bypass vercel deployment protection, fix can send message on uploading files.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Bypass vercel deployment protection, closes [#3627](https://github.com/lobehub/lobe-chat/issues/3627) ([47da20d](https://github.com/lobehub/lobe-chat/commit/47da20d))
21
+ - **misc**: Fix can send message on uploading files, closes [#3618](https://github.com/lobehub/lobe-chat/issues/3618) ([fe4329a](https://github.com/lobehub/lobe-chat/commit/fe4329a))
22
+
23
+ </details>
24
+
25
+ <div align="right">
26
+
27
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
28
+
29
+ </div>
30
+
31
+ ### [Version 1.13.1](https://github.com/lobehub/lobe-chat/compare/v1.13.0...v1.13.1)
32
+
33
+ <sup>Released on **2024-08-27**</sup>
34
+
35
+ #### 💄 Styles
36
+
37
+ - **misc**: Update Qwen models.
38
+
39
+ <br/>
40
+
41
+ <details>
42
+ <summary><kbd>Improvements and Fixes</kbd></summary>
43
+
44
+ #### Styles
45
+
46
+ - **misc**: Update Qwen models, closes [#3626](https://github.com/lobehub/lobe-chat/issues/3626) ([4393386](https://github.com/lobehub/lobe-chat/commit/4393386))
47
+
48
+ </details>
49
+
50
+ <div align="right">
51
+
52
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
53
+
54
+ </div>
55
+
5
56
  ## [Version 1.13.0](https://github.com/lobehub/lobe-chat/compare/v1.12.20...v1.13.0)
6
57
 
7
58
  <sup>Released on **2024-08-27**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
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",
@@ -100,6 +100,7 @@
100
100
  },
101
101
  "dependencies": {
102
102
  "@ant-design/icons": "^5.4.0",
103
+ "@ant-design/pro-components": "^2.7.10",
103
104
  "@anthropic-ai/sdk": "^0.24.3",
104
105
  "@auth/core": "0.28.0",
105
106
  "@aws-sdk/client-bedrock-runtime": "^3.637.0",
@@ -123,6 +124,7 @@
123
124
  "@lobehub/ui": "^1.149.2",
124
125
  "@neondatabase/serverless": "^0.9.4",
125
126
  "@next/third-parties": "^14.2.6",
127
+ "@react-spring/web": "^9.7.3",
126
128
  "@sentry/nextjs": "^7.119.0",
127
129
  "@t3-oss/env-nextjs": "^0.11.0",
128
130
  "@tanstack/react-query": "^5.52.1",
@@ -147,6 +149,7 @@
147
149
  "drizzle-zod": "^0.5.1",
148
150
  "fast-deep-equal": "^3.1.3",
149
151
  "file-type": "^19.4.1",
152
+ "framer-motion": "^11.2.6",
150
153
  "gpt-tokenizer": "^2.2.1",
151
154
  "i18next": "^23.14.0",
152
155
  "i18next-browser-languagedetector": "^7.2.1",
@@ -172,6 +175,7 @@
172
175
  "officeparser": "^4.1.1",
173
176
  "ollama": "^0.5.8",
174
177
  "openai": "^4.56.0",
178
+ "openapi-fetch": "^0.9.7",
175
179
  "partial-json": "^0.1.7",
176
180
  "pdf-parse": "^1.1.1",
177
181
  "pdfjs-dist": "4.4.168",
@@ -183,6 +187,7 @@
183
187
  "query-string": "^9.1.0",
184
188
  "random-words": "^2.0.1",
185
189
  "react": "^18.3.1",
190
+ "react-confetti": "^6.1.0",
186
191
  "react-dom": "^18.3.1",
187
192
  "react-fast-marquee": "^1.6.5",
188
193
  "react-hotkeys-hook": "^4.5.0",
@@ -199,6 +204,7 @@
199
204
  "rtl-detect": "^1.1.2",
200
205
  "semver": "^7.6.3",
201
206
  "sharp": "^0.33.5",
207
+ "stripe": "^15.8.0",
202
208
  "superjson": "^2.2.1",
203
209
  "svix": "^1.30.0",
204
210
  "swr": "^2.2.5",
@@ -270,12 +276,14 @@
270
276
  "markdown-table": "^3.0.3",
271
277
  "node-fetch": "^3.3.2",
272
278
  "node-gyp": "^10.2.0",
279
+ "openapi-typescript": "^6.7.6",
273
280
  "p-map": "^7.0.2",
274
281
  "prettier": "^3.3.3",
275
282
  "remark-cli": "^11.0.0",
276
283
  "remark-parse": "^10.0.2",
277
284
  "semantic-release": "^21.1.2",
278
285
  "stylelint": "^15.11.0",
286
+ "supports-color": "8",
279
287
  "tsx": "^4.17.0",
280
288
  "typescript": "^5.5.4",
281
289
  "unified": "^11.0.5",
@@ -6,8 +6,8 @@ import { memo, useRef } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
- import SettingContainer from '@/features/Setting//SettingContainer';
10
9
  import Footer from '@/features/Setting/Footer';
10
+ import SettingContainer from '@/features/Setting/SettingContainer';
11
11
  import { useActiveSettingsKey } from '@/hooks/useActiveSettingsKey';
12
12
  import { SettingsTabs } from '@/store/global/initialState';
13
13
 
@@ -1,8 +1,8 @@
1
1
  import {
2
- AiMass,
3
2
  Adobe,
4
3
  Ai21,
5
4
  Ai360,
5
+ AiMass,
6
6
  Aws,
7
7
  Aya,
8
8
  Azure,
@@ -49,6 +49,7 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model: originModel, size = 12
49
49
  const model = originModel.toLowerCase();
50
50
 
51
51
  // currently supported models, maybe not in its own provider
52
+ if (model.includes('text-embedding-')) return <OpenAI.Avatar size={size} />;
52
53
  if (model.includes('gpt-3')) return <OpenAI.Avatar size={size} type={'gpt3'} />;
53
54
  if (model.includes('gpt-4')) return <OpenAI.Avatar size={size} type={'gpt4'} />;
54
55
  if (model.includes('glm-') || model.includes('chatglm')) return <ChatGLM.Avatar size={size} />;
@@ -63,7 +64,13 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model: originModel, size = 12
63
64
  if (model.includes('moonshot')) return <Moonshot.Avatar size={size} />;
64
65
  if (model.includes('qwen')) return <Tongyi.Avatar background={Tongyi.colorPrimary} size={size} />;
65
66
  if (model.includes('minmax') || model.includes('abab')) return <Minimax.Avatar size={size} />;
66
- if (model.includes('mistral') || model.includes('mixtral') || model.includes('codestral') || model.includes('mathstral')) return <Mistral.Avatar size={size} />;
67
+ if (
68
+ model.includes('mistral') ||
69
+ model.includes('mixtral') ||
70
+ model.includes('codestral') ||
71
+ model.includes('mathstral')
72
+ )
73
+ return <Mistral.Avatar size={size} />;
67
74
  if (model.includes('pplx') || model.includes('sonar')) return <Perplexity.Avatar size={size} />;
68
75
  if (model.includes('yi-')) return <Yi.Avatar size={size} />;
69
76
  if (model.startsWith('openrouter')) return <OpenRouter.Avatar size={size} />; // only for Cinematika and Auto
@@ -98,7 +105,8 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model: originModel, size = 12
98
105
  )
99
106
  return <Stability.Avatar size={size} />;
100
107
 
101
- if (model.includes('phi3') || model.includes('phi-3') || model.includes('wizardlm')) return <Azure.Avatar size={size} />;
108
+ if (model.includes('phi3') || model.includes('phi-3') || model.includes('wizardlm'))
109
+ return <Azure.Avatar size={size} />;
102
110
  if (model.includes('firefly')) return <Adobe.Avatar size={size} />;
103
111
  if (model.includes('jamba') || model.includes('j2-')) return <Ai21.Avatar size={size} />;
104
112
  });
@@ -60,7 +60,7 @@ const Qwen: ModelProviderCard = {
60
60
  displayName: 'Qwen VL Plus',
61
61
  enabled: true,
62
62
  id: 'qwen-vl-plus',
63
- tokens: 6144,
63
+ tokens: 8192,
64
64
  vision: true,
65
65
  },
66
66
  {
@@ -69,7 +69,41 @@ const Qwen: ModelProviderCard = {
69
69
  displayName: 'Qwen VL Max',
70
70
  enabled: true,
71
71
  id: 'qwen-vl-max',
72
- tokens: 6144,
72
+ tokens: 8192,
73
+ vision: true,
74
+ },
75
+ {
76
+ description:
77
+ '以 Qwen-7B 语言模型初始化,添加图像模型,图像输入分辨率为448的预训练模型。',
78
+ displayName: 'Qwen VL',
79
+ enabled: true,
80
+ id: 'qwen-vl-v1',
81
+ tokens: 8192,
82
+ vision: true,
83
+ },
84
+ {
85
+ description:
86
+ '通义千问VL支持灵活的交互方式,包括多图、多轮问答、创作等能力的模型。',
87
+ displayName: 'Qwen VL Chat',
88
+ enabled: true,
89
+ id: 'qwen-vl-chat-v1',
90
+ tokens: 8192,
91
+ vision: true,
92
+ },
93
+ {
94
+ description: 'Qwen2-Math 模型具有强大的数学解题能力',
95
+ displayName: 'Qwen2 Math 72B',
96
+ enabled: true,
97
+ id: 'qwen2-math-72b-instruct',
98
+ tokens: 4096,
99
+ },
100
+ {
101
+ description:
102
+ '抢先体验即将升级的 qwen-vl-max 大模型。',
103
+ displayName: 'Qwen VL Max 0809',
104
+ enabled: true,
105
+ id: 'qwen-vl-max-0809',
106
+ tokens: 32_768,
73
107
  vision: true,
74
108
  },
75
109
  ],
@@ -570,4 +570,37 @@ describe('FileModel', () => {
570
570
  const data = await fileModel.countFilesByHash('hash1');
571
571
  expect(data).toEqual(2);
572
572
  });
573
+
574
+ describe('countUsage', () => {
575
+ const sharedFileList = [
576
+ {
577
+ name: 'document.pdf',
578
+ url: 'https://example.com/document.pdf',
579
+ size: 1000,
580
+ fileType: 'application/pdf',
581
+ userId,
582
+ },
583
+ {
584
+ name: 'image.jpg',
585
+ url: 'https://example.com/image.jpg',
586
+ size: 500,
587
+ fileType: 'image/jpeg',
588
+ userId,
589
+ },
590
+ {
591
+ name: 'audio.mp3',
592
+ url: 'https://example.com/audio.mp3',
593
+ size: 2000,
594
+ fileType: 'audio/mpeg',
595
+ userId,
596
+ },
597
+ ];
598
+
599
+ it('should get total size of files for the user', async () => {
600
+ await serverDB.insert(files).values(sharedFileList);
601
+ const size = await fileModel.countUsage();
602
+
603
+ expect(size).toBe(3500);
604
+ });
605
+ });
573
606
  });
@@ -1,4 +1,4 @@
1
- import { eq } from 'drizzle-orm';
1
+ import { count, eq } from 'drizzle-orm';
2
2
  import { and } from 'drizzle-orm/expressions';
3
3
 
4
4
  import { serverDB } from '@/database/server';
@@ -47,4 +47,15 @@ export class EmbeddingModel {
47
47
  where: and(eq(embeddings.id, id), eq(embeddings.userId, this.userId)),
48
48
  });
49
49
  };
50
+
51
+ countUsage = async () => {
52
+ const result = await serverDB
53
+ .select({
54
+ count: count(),
55
+ })
56
+ .from(embeddings)
57
+ .where(eq(embeddings.userId, this.userId));
58
+
59
+ return result[0].count;
60
+ };
50
61
  }
@@ -1,4 +1,4 @@
1
- import { asc, count, eq, ilike, inArray, notExists } from 'drizzle-orm';
1
+ import { asc, count, eq, ilike, inArray, notExists, sum } from 'drizzle-orm';
2
2
  import { and, desc } from 'drizzle-orm/expressions';
3
3
 
4
4
  import { serverDBEnv } from '@/config/db';
@@ -91,6 +91,17 @@ export class FileModel {
91
91
  return serverDB.delete(globalFiles).where(eq(globalFiles.hashId, hashId));
92
92
  };
93
93
 
94
+ countUsage = async () => {
95
+ const result = await serverDB
96
+ .select({
97
+ totalSize: sum(files.size),
98
+ })
99
+ .from(files)
100
+ .where(eq(files.userId, this.userId));
101
+
102
+ return parseInt(result[0].totalSize!) || 0;
103
+ };
104
+
94
105
  deleteMany = async (ids: string[]) => {
95
106
  const fileList = await this.findByIds(ids);
96
107
  const hashList = fileList.map((file) => file.fileHash!);
@@ -18,10 +18,24 @@ export const useSendMessage = () => {
18
18
 
19
19
  const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
20
20
 
21
+ const isUploadingFiles = useFileStore(fileChatSelectors.isUploadingFiles);
22
+ const isSendButtonDisabledByMessage = useChatStore(chatSelectors.isSendButtonDisabledByMessage);
23
+
24
+ const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
25
+
21
26
  const send = useCallback((params: UseSendMessageParams = {}) => {
22
27
  const store = useChatStore.getState();
23
28
  if (chatSelectors.isAIGenerating(store)) return;
24
29
 
30
+ // if uploading file or send button is disabled by message, then we should not send the message
31
+ const isUploadingFiles = fileChatSelectors.isUploadingFiles(useFileStore.getState());
32
+ const isSendButtonDisabledByMessage = chatSelectors.isSendButtonDisabledByMessage(
33
+ useChatStore.getState(),
34
+ );
35
+
36
+ const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
37
+ if (!canSend) return;
38
+
25
39
  const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
26
40
  // if there is no message and no image, then we should not send the message
27
41
  if (!store.inputMessage && fileList.length === 0) return;
@@ -44,10 +58,5 @@ export const useSendMessage = () => {
44
58
  // }
45
59
  }, []);
46
60
 
47
- const isUploadingFiles = useFileStore(fileChatSelectors.isUploadingFiles);
48
- const isSendButtonDisabledByMessage = useChatStore(chatSelectors.isSendButtonDisabledByMessage);
49
-
50
- const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
51
-
52
61
  return useMemo(() => ({ canSend, send }), [canSend]);
53
62
  };
@@ -4,32 +4,37 @@ import { useResponsive } from 'antd-style';
4
4
  import { PropsWithChildren, ReactNode, memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
- const SettingContainer = memo<
8
- PropsWithChildren<{ addonAfter?: ReactNode; addonBefore?: ReactNode }>
9
- >(({ children, addonAfter, addonBefore }) => {
10
- const { mobile = false } = useResponsive();
11
- return (
12
- <Flexbox
13
- align={'center'}
14
- height={'100%'}
15
- paddingBlock={mobile ? undefined : 32}
16
- style={{ overflowX: 'hidden', overflowY: 'auto' }}
17
- width={'100%'}
18
- >
19
- {addonBefore}
7
+ interface SettingContainerProps {
8
+ addonAfter?: ReactNode;
9
+ addonBefore?: ReactNode;
10
+ fullWidth?: boolean;
11
+ }
12
+ const SettingContainer = memo<PropsWithChildren<SettingContainerProps>>(
13
+ ({ children, addonAfter, addonBefore, fullWidth }) => {
14
+ const { mobile = false } = useResponsive();
15
+ return (
20
16
  <Flexbox
21
- gap={64}
22
- paddingInline={mobile ? undefined : 24}
23
- style={{
24
- maxWidth: 1024,
25
- }}
17
+ align={'center'}
18
+ height={'100%'}
19
+ paddingBlock={mobile ? undefined : 32}
20
+ style={{ overflowX: 'hidden', overflowY: 'auto' }}
26
21
  width={'100%'}
27
22
  >
28
- {children}
23
+ {addonBefore}
24
+ <Flexbox
25
+ gap={64}
26
+ paddingInline={mobile ? undefined : 24}
27
+ style={{
28
+ maxWidth: fullWidth ? undefined : 1024,
29
+ }}
30
+ width={'100%'}
31
+ >
32
+ {children}
33
+ </Flexbox>
34
+ {addonAfter}
29
35
  </Flexbox>
30
- {addonAfter}
31
- </Flexbox>
32
- );
33
- });
36
+ );
37
+ },
38
+ );
34
39
 
35
40
  export default SettingContainer;
@@ -4,7 +4,6 @@ import { User } from 'next-auth';
4
4
  import { NextRequest } from 'next/server';
5
5
 
6
6
  import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
7
- import NextAuthEdge from '@/libs/next-auth/edge';
8
7
 
9
8
  type ClerkAuth = ReturnType<typeof getAuth>;
10
9
 
@@ -55,6 +54,8 @@ export const createContext = async (request: NextRequest): Promise<Context> => {
55
54
 
56
55
  if (enableNextAuth) {
57
56
  try {
57
+ const { default: NextAuthEdge } = await import('@/libs/next-auth/edge');
58
+
58
59
  const session = await NextAuthEdge.auth();
59
60
  if (session && session?.user?.id) {
60
61
  auth = session.user;
@@ -11,14 +11,17 @@ import type { AsyncRouter } from './index';
11
11
 
12
12
  export const createAsyncServerClient = async (userId: string, payload: JWTPayload) => {
13
13
  const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
14
-
14
+ const headers: Record<string, string> = {
15
+ Authorization: `Bearer ${serverDBEnv.KEY_VAULTS_SECRET}`,
16
+ [LOBE_CHAT_AUTH_HEADER]: await gateKeeper.encrypt(JSON.stringify({ payload, userId })),
17
+ };
18
+ if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
19
+ headers['x-vercel-protection-bypass'] = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
20
+ }
15
21
  return createTRPCClient<AsyncRouter>({
16
22
  links: [
17
23
  httpBatchLink({
18
- headers: {
19
- Authorization: `Bearer ${serverDBEnv.KEY_VAULTS_SECRET}`,
20
- [LOBE_CHAT_AUTH_HEADER]: await gateKeeper.encrypt(JSON.stringify({ payload, userId })),
21
- },
24
+ headers,
22
25
  transformer: superjson,
23
26
  url: urlJoin(appEnv.APP_URL!, '/trpc/async'),
24
27
  }),
@@ -175,7 +175,6 @@ const isHasMessageLoading = (s: ChatStore) => s.messageLoadingIds.length > 0;
175
175
 
176
176
  /**
177
177
  * this function is used to determine whether the send button should be disabled
178
- * @param s
179
178
  */
180
179
  const isSendButtonDisabledByMessage = (s: ChatStore) =>
181
180
  // 1. when there is message loading
@@ -46,3 +46,11 @@ export const formatTime = (timeInSeconds: number): string => {
46
46
  return `${(timeInSeconds / 3600).toFixed(2)} h`;
47
47
  }
48
48
  };
49
+
50
+ /**
51
+ * format number with comma
52
+ * @param num
53
+ */
54
+ export const formatNumber = (num: any) => {
55
+ return new Intl.NumberFormat('en-US').format(num);
56
+ };