@lobehub/chat 0.151.10 → 0.152.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,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.152.0](https://github.com/lobehub/lobe-chat/compare/v0.151.11...v0.152.0)
6
+
7
+ <sup>Released on **2024-04-30**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Import settings from url.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Import settings from url, closes [#2226](https://github.com/lobehub/lobe-chat/issues/2226) ([b1f6c20](https://github.com/lobehub/lobe-chat/commit/b1f6c20))
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
+
30
+ ### [Version 0.151.11](https://github.com/lobehub/lobe-chat/compare/v0.151.10...v0.151.11)
31
+
32
+ <sup>Released on **2024-04-30**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix telemetry preference modal and default agent config error.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix telemetry preference modal and default agent config error, closes [#2312](https://github.com/lobehub/lobe-chat/issues/2312) ([8900445](https://github.com/lobehub/lobe-chat/commit/8900445))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.151.10](https://github.com/lobehub/lobe-chat/compare/v0.151.9...v0.151.10)
6
56
 
7
57
  <sup>Released on **2024-04-30**</sup>
package/README.md CHANGED
@@ -261,14 +261,14 @@ Our marketplace is not just a showcase platform but also a collaborative space.
261
261
 
262
262
  <!-- AGENT LIST -->
263
263
 
264
- | Recent Submits | Description |
265
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
266
- | [Jailbreak Assistant DAN](https://chat-preview.lobehub.com/market?agent=gpt-4-dan-assistant)<br/><sup>By **[MapleEve](https://github.com/MapleEve)** on **2024-04-26**</sup> | Bypass OpenAI review mechanism, ChatGPT after jailbreak<br/>`creative` `artificial-intelligence` `conversation` `jailbreak` |
267
- | [TailwindHelper](https://chat-preview.lobehub.com/market?agent=tailwind-helper)<br/><sup>By **[aototo](https://github.com/aototo)** on **2024-04-26**</sup> | TailwindHelper is a professional frontend designer with a solid foundation in design theory and rich practical experience. Created by a leading software development company, it aims to help developers and designers accelerate the development process of web interfaces. TailwindHelper is proficient in the Tailwind CSS framework and can translate complex design requirements into efficient and responsive CSS class names.<br/>`tailwindcss` `css` `tailwind-helper` |
268
- | [yapi JSON-SCHEMA to Typescript](https://chat-preview.lobehub.com/market?agent=yapi-ts-helper)<br/><sup>By **[zcf0508](https://github.com/zcf0508)** on **2024-04-26**</sup> | Specializes in converting JSON schema to TypeScript types.<br/>`typescript` `development` |
269
- | [Chinese Academic Paper Editor](https://chat-preview.lobehub.com/market?agent=chinese-paper-polishing)<br/><sup>By **[y22emc2](https://github.com/y22emc2)** on **2024-04-15**</sup> | As an assistant for improving Chinese academic paper writing, your task is to enhance the provided text in terms of spelling, grammar, clarity, conciseness, and overall readability, to improve the text's academic standards and literary quality. This includes breaking down long sentences, reducing repetition, and providing improvement suggestions. Please provide the corrected version of the text first, then list the modifications in the markdown table below, along with the reasons for the changes.<br/>`academic-paper-writing` `proofreading` `text-editing` |
270
-
271
- > 📊 Total agents: [<kbd>**243**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
264
+ | Recent Submits | Description |
265
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
266
+ | [Sales Description Specialist](https://chat-preview.lobehub.com/market?agent=verkauf-kleinanzeigen)<br/><sup>By **[highseen](https://github.com/highseen)** on **2024-04-30**</sup> | Assists in the sale of used items through research, pricing, description, and title creation.<br/>`product-sales` `research` `description` |
267
+ | [Jailbreak Assistant DAN](https://chat-preview.lobehub.com/market?agent=gpt-4-dan-assistant)<br/><sup>By **[MapleEve](https://github.com/MapleEve)** on **2024-04-26**</sup> | Bypass OpenAI review mechanism, ChatGPT after jailbreak<br/>`creative` `artificial-intelligence` `conversation` `jailbreak` |
268
+ | [TailwindHelper](https://chat-preview.lobehub.com/market?agent=tailwind-helper)<br/><sup>By **[aototo](https://github.com/aototo)** on **2024-04-26**</sup> | TailwindHelper is a professional frontend designer with a solid foundation in design theory and rich practical experience. Created by a leading software development company, it aims to help developers and designers accelerate the development process of web interfaces. TailwindHelper is proficient in the Tailwind CSS framework and can translate complex design requirements into efficient and responsive CSS class names.<br/>`tailwindcss` `css` `tailwind-helper` |
269
+ | [yapi JSON-SCHEMA to Typescript](https://chat-preview.lobehub.com/market?agent=yapi-ts-helper)<br/><sup>By **[zcf0508](https://github.com/zcf0508)** on **2024-04-26**</sup> | Specializes in converting JSON schema to TypeScript types.<br/>`typescript` `development` |
270
+
271
+ > 📊 Total agents: [<kbd>**244**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
272
272
 
273
273
  <!-- AGENT LIST -->
274
274
 
package/README.zh-CN.md CHANGED
@@ -251,12 +251,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
251
251
 
252
252
  | 最近新增 | 助手说明 |
253
253
  | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
254
+ | [销售描述专家](https://chat-preview.lobehub.com/market?agent=verkauf-kleinanzeigen)<br/><sup>By **[highseen](https://github.com/highseen)** on **2024-04-30**</sup> | 通过研究、定价、描述和标题设计帮助销售二手物品。<br/>`产品销售` `研究` `描述` |
254
255
  | [越狱助手 DAN](https://chat-preview.lobehub.com/market?agent=gpt-4-dan-assistant)<br/><sup>By **[MapleEve](https://github.com/MapleEve)** on **2024-04-26**</sup> | 突破 OpenAI 审核机制,越狱之后的 ChatGPT<br/>`创意` `人工智能` `对话` `越狱` |
255
256
  | [TailwindHelper](https://chat-preview.lobehub.com/market?agent=tailwind-helper)<br/><sup>By **[aototo](https://github.com/aototo)** on **2024-04-26**</sup> | TailwindHelper 是一位专业的前端设计师,拥有深厚的设计理论基础和丰富的实践经验。它由一家领先的软件开发公司创建,旨在帮助开发者和设计师加速 Web 界面的开发过程。TailwindHelper 精通 Tailwind CSS 框架,并能够理解复杂的设计要求,转化为高效且响应式的 CSS 类名。<br/>`tailwindcss` `css` `tailwind-helper` |
256
257
  | [yapi JSON-SCHEMA to Typescript](https://chat-preview.lobehub.com/market?agent=yapi-ts-helper)<br/><sup>By **[zcf0508](https://github.com/zcf0508)** on **2024-04-26**</sup> | 擅长将 JSON schema 转换为 TypeScript 类型。<br/>`typescript` `开发` |
257
- | [中文论文编辑师](https://chat-preview.lobehub.com/market?agent=chinese-paper-polishing)<br/><sup>By **[y22emc2](https://github.com/y22emc2)** on **2024-04-15**</sup> | 作为一名中文学术论文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,提高文本的学术规范性、文学性,同时分解长句,减少重复,并提供改进建议。请先提供文本的更正版本,然后在 markdown 表格中列出修改的内容,并给出修改的理由。<br/>`学术论文写作` `校对` `文本编辑` |
258
258
 
259
- > 📊 Total agents: [<kbd>**243**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
259
+ > 📊 Total agents: [<kbd>**244**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
260
260
 
261
261
  <!-- AGENT LIST -->
262
262
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.151.10",
3
+ "version": "0.152.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",
@@ -12,6 +12,7 @@ import { PRIVACY_URL } from '@/const/url';
12
12
  import { useServerConfigStore } from '@/store/serverConfig';
13
13
  import { serverConfigSelectors } from '@/store/serverConfig/selectors';
14
14
  import { useUserStore } from '@/store/user';
15
+ import { preferenceSelectors } from '@/store/user/selectors';
15
16
 
16
17
  const useStyles = createStyles(({ css, token, isDarkMode }) => ({
17
18
  container: css`
@@ -57,13 +58,14 @@ const TelemetryNotification = memo<{ mobile?: boolean }>(({ mobile }) => {
57
58
 
58
59
  const { t } = useTranslation('common');
59
60
  const shouldCheck = useServerConfigStore(serverConfigSelectors.enabledTelemetryChat);
61
+ const isPreferenceInit = useUserStore(preferenceSelectors.isPreferenceInit);
60
62
 
61
63
  const [useCheckTrace, updatePreference] = useUserStore((s) => [
62
64
  s.useCheckTrace,
63
65
  s.updatePreference,
64
66
  ]);
65
67
 
66
- const { data: showModal, mutate } = useCheckTrace(shouldCheck);
68
+ const { data: showModal, mutate } = useCheckTrace(shouldCheck && isPreferenceInit);
67
69
 
68
70
  const updateTelemetry = (telemetry: boolean) => {
69
71
  updatePreference({ telemetry });
@@ -3,13 +3,11 @@ import { memo } from 'react';
3
3
 
4
4
  import { INBOX_SESSION_ID } from '@/const/session';
5
5
  import AgentSetting from '@/features/AgentSetting';
6
- import { useAgentStore } from '@/store/agent';
7
- import { agentSelectors } from '@/store/agent/selectors';
8
6
  import { useUserStore } from '@/store/user';
9
7
  import { settingsSelectors } from '@/store/user/selectors';
10
8
 
11
9
  const Agent = memo(() => {
12
- const config = useAgentStore(agentSelectors.defaultAgentConfig, isEqual);
10
+ const config = useUserStore(settingsSelectors.defaultAgentConfig, isEqual);
13
11
  const meta = useUserStore(settingsSelectors.defaultAgentMeta, isEqual);
14
12
  const [updateAgent] = useUserStore((s) => [s.updateDefaultAgent]);
15
13
 
package/src/const/url.ts CHANGED
@@ -43,6 +43,7 @@ export const SESSION_CHAT_URL = (id: string = INBOX_SESSION_ID, mobile?: boolean
43
43
 
44
44
  export const imageUrl = (filename: string) => withBasePath(`/images/${filename}`);
45
45
 
46
+ export const LOBE_URL_IMPORT_NAME = 'settings';
46
47
  export const EMAIL_SUPPORT = 'support@lobehub.com';
47
48
  export const EMAIL_BUSINESS = 'hello@lobehub.com';
48
49
 
@@ -12,7 +12,7 @@ const Temperature = memo(() => {
12
12
 
13
13
  const [temperature, updateAgentConfig] = useAgentStore((s) => {
14
14
  const config = agentSelectors.currentAgentConfig(s);
15
- return [config.params.temperature, s.updateAgentConfig];
15
+ return [config.params?.temperature, s.updateAgentConfig];
16
16
  });
17
17
 
18
18
  return (
@@ -1,13 +1,16 @@
1
1
  import { useMemo } from 'react';
2
2
 
3
3
  import { ImportResults, configService } from '@/services/config';
4
+ import { shareService } from '@/services/share';
4
5
  import { useChatStore } from '@/store/chat';
5
6
  import { useSessionStore } from '@/store/session';
7
+ import { useUserStore } from '@/store/user';
6
8
  import { importConfigFile } from '@/utils/config';
7
9
 
8
10
  export const useImportConfig = () => {
9
11
  const refreshSessions = useSessionStore((s) => s.refreshSessions);
10
12
  const [refreshMessages, refreshTopics] = useChatStore((s) => [s.refreshMessages, s.refreshTopic]);
13
+ const [setSettings] = useUserStore((s) => [s.setSettings]);
11
14
 
12
15
  const importConfig = async (file: File) =>
13
16
  new Promise<ImportResults | undefined>((resolve) => {
@@ -22,5 +25,21 @@ export const useImportConfig = () => {
22
25
  });
23
26
  });
24
27
 
25
- return useMemo(() => ({ importConfig }), []);
28
+ /**
29
+ * Import settings from a string in json format
30
+ * @param settingsParams
31
+ * @returns
32
+ */
33
+ const importSettings = (settingsParams: string | null) => {
34
+ if (settingsParams) {
35
+ const importSettings = shareService.decodeShareSettings(settingsParams);
36
+ if (importSettings?.message || !importSettings?.data) {
37
+ // handle some error
38
+ return;
39
+ }
40
+ setSettings(importSettings.data);
41
+ }
42
+ };
43
+
44
+ return useMemo(() => ({ importConfig, importSettings }), []);
26
45
  };
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { useRouter } from 'next/navigation';
3
+ import { useRouter, useSearchParams } from 'next/navigation';
4
4
  import { memo, useEffect } from 'react';
5
5
  import { createStoreUpdater } from 'zustand-utils';
6
6
 
7
+ import { LOBE_URL_IMPORT_NAME } from '@/const/url';
8
+ import { useImportConfig } from '@/hooks/useImportConfig';
7
9
  import { useIsMobile } from '@/hooks/useIsMobile';
8
10
  import { useEnabledDataSync } from '@/hooks/useSyncData';
9
11
  import { useAgentStore } from '@/store/agent';
@@ -38,6 +40,13 @@ const StoreInitialization = memo(() => {
38
40
  useStoreUpdater('isMobile', mobile);
39
41
  useStoreUpdater('router', router);
40
42
 
43
+ // Import settings from the url
44
+ const { importSettings } = useImportConfig();
45
+ const searchParam = useSearchParams().get(LOBE_URL_IMPORT_NAME);
46
+ useEffect(() => {
47
+ importSettings(searchParam);
48
+ }, [searchParam]);
49
+
41
50
  useEffect(() => {
42
51
  router.prefetch('/chat');
43
52
  router.prefetch('/chat/settings');
@@ -1,9 +1,12 @@
1
+ import { DeepPartial } from 'utility-types';
1
2
  import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
3
 
4
+ import { LOBE_URL_IMPORT_NAME } from '@/const/url';
5
+ import { GlobalSettings } from '@/types/settings';
3
6
  import { ShareGPTConversation } from '@/types/share';
4
7
  import { parseMarkdown } from '@/utils/parseMarkdown';
5
8
 
6
- import { SHARE_GPT_URL, shareGPTService } from '../share';
9
+ import { SHARE_GPT_URL, shareService } from '../share';
7
10
 
8
11
  // Mock dependencies
9
12
  vi.mock('@/utils/parseMarkdown', () => ({
@@ -32,7 +35,7 @@ describe('ShareGPTService', () => {
32
35
  });
33
36
 
34
37
  // Act
35
- const url = await shareGPTService.createShareGPTUrl(conversation);
38
+ const url = await shareService.createShareGPTUrl(conversation);
36
39
 
37
40
  // Assert
38
41
  expect(parseMarkdown).toHaveBeenCalledWith('Hi there!');
@@ -48,7 +51,7 @@ describe('ShareGPTService', () => {
48
51
  (fetch as Mock).mockRejectedValue(new Error('Network error'));
49
52
 
50
53
  // Act & Assert
51
- await expect(shareGPTService.createShareGPTUrl(conversation)).rejects.toThrow('Network error');
54
+ await expect(shareService.createShareGPTUrl(conversation)).rejects.toThrow('Network error');
52
55
  });
53
56
 
54
57
  it('should not parse markdown for items not from gpt', async () => {
@@ -65,7 +68,7 @@ describe('ShareGPTService', () => {
65
68
  });
66
69
 
67
70
  // Act
68
- await shareGPTService.createShareGPTUrl(conversation);
71
+ await shareService.createShareGPTUrl(conversation);
69
72
 
70
73
  // Assert
71
74
  expect(parseMarkdown).not.toHaveBeenCalled();
@@ -81,6 +84,48 @@ describe('ShareGPTService', () => {
81
84
  });
82
85
 
83
86
  // Act & Assert
84
- await expect(shareGPTService.createShareGPTUrl(conversation)).rejects.toThrow();
87
+ await expect(shareService.createShareGPTUrl(conversation)).rejects.toThrow();
88
+ });
89
+ });
90
+
91
+ describe('ShareViaUrl', () => {
92
+ describe('createShareSettingsUrl', () => {
93
+ it('should create a share settings URL with the provided settings', () => {
94
+ const settings: DeepPartial<GlobalSettings> = {
95
+ languageModel: {
96
+ openai: {
97
+ apiKey: 'user-key',
98
+ },
99
+ },
100
+ };
101
+ const url = shareService.createShareSettingsUrl(settings);
102
+ expect(url).toBe(
103
+ `/?${LOBE_URL_IMPORT_NAME}=%7B%22languageModel%22:%7B%22openai%22:%7B%22apiKey%22:%22user-key%22%7D%7D%7D`,
104
+ );
105
+ });
106
+ });
107
+
108
+ describe('decodeShareSettings', () => {
109
+ it('should decode share settings from search params', () => {
110
+ const settings = '{"languageModel":{"openai":{"apiKey":"user-key"}}}';
111
+ const decodedSettings = shareService.decodeShareSettings(settings);
112
+ expect(decodedSettings).toEqual({
113
+ data: {
114
+ languageModel: {
115
+ openai: {
116
+ apiKey: 'user-key',
117
+ },
118
+ },
119
+ },
120
+ });
121
+ });
122
+
123
+ it('should return an error message if decoding fails', () => {
124
+ const settings = '%7B%22theme%22%3A%22dark%22%2C%22fontSize%22%3A16%';
125
+ const decodedSettings = shareService.decodeShareSettings(settings);
126
+ expect(decodedSettings).toEqual({
127
+ message: expect.any(String),
128
+ });
129
+ });
85
130
  });
86
131
  });
@@ -243,6 +243,32 @@ describe('SessionService', () => {
243
243
  });
244
244
  });
245
245
 
246
+ describe('hasSessions', () => {
247
+ it('should return false if no sessions exist', async () => {
248
+ // Setup
249
+ (SessionModel.count as Mock).mockResolvedValue(0);
250
+
251
+ // Execute
252
+ const result = await sessionService.hasSessions();
253
+
254
+ // Assert
255
+ expect(SessionModel.count).toHaveBeenCalled();
256
+ expect(result).toBe(false);
257
+ });
258
+
259
+ it('should return true if sessions exist', async () => {
260
+ // Setup
261
+ (SessionModel.count as Mock).mockResolvedValue(1);
262
+
263
+ // Execute
264
+ const result = await sessionService.hasSessions();
265
+
266
+ // Assert
267
+ expect(SessionModel.count).toHaveBeenCalled();
268
+ expect(result).toBe(true);
269
+ });
270
+ });
271
+
246
272
  describe('searchSessions', () => {
247
273
  it('should return sessions that match the keyword', async () => {
248
274
  // Setup
@@ -80,7 +80,7 @@ export class ClientService implements ISessionService {
80
80
  return SessionModel.count();
81
81
  }
82
82
  async hasSessions() {
83
- return (await this.countSessions()) === 0;
83
+ return (await this.countSessions()) !== 0;
84
84
  }
85
85
 
86
86
  async searchSessions(keyword: string) {
@@ -1,9 +1,14 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { LOBE_URL_IMPORT_NAME } from '@/const/url';
4
+ import { GlobalSettings } from '@/types/settings';
1
5
  import { ShareGPTConversation } from '@/types/share';
6
+ import { withBasePath } from '@/utils/basePath';
2
7
  import { parseMarkdown } from '@/utils/parseMarkdown';
3
8
 
4
9
  export const SHARE_GPT_URL = 'https://sharegpt.com/api/conversations';
5
10
 
6
- class ShareGPTService {
11
+ class ShareService {
7
12
  public async createShareGPTUrl(conversation: ShareGPTConversation) {
8
13
  const items = [];
9
14
 
@@ -29,6 +34,28 @@ class ShareGPTService {
29
34
  // short link to the ShareGPT post
30
35
  return `https://shareg.pt/${id}`;
31
36
  }
37
+
38
+ /**
39
+ * Creates a share settings URL with the provided settings.
40
+ * @param settings - The settings object to be encoded in the URL.
41
+ * @returns The share settings URL.
42
+ */
43
+ public createShareSettingsUrl(settings: DeepPartial<GlobalSettings>) {
44
+ return withBasePath(`/?${LOBE_URL_IMPORT_NAME}=${encodeURI(JSON.stringify(settings))}`);
45
+ }
46
+
47
+ /**
48
+ * Decode share settings from search params
49
+ * @param settings
50
+ * @returns
51
+ */
52
+ public decodeShareSettings(settings: string) {
53
+ try {
54
+ return { data: JSON.parse(settings) as DeepPartial<GlobalSettings> };
55
+ } catch (e) {
56
+ return { message: JSON.stringify(e) };
57
+ }
58
+ }
32
59
  }
33
60
 
34
- export const shareGPTService = new ShareGPTService();
61
+ export const shareService = new ShareService();
@@ -1,18 +1,16 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
2
 
3
3
  import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
4
- import { shareGPTService } from '@/services/share';
4
+ import { shareService } from '@/services/share';
5
5
  import { useChatStore } from '@/store/chat';
6
6
  import { ChatMessage } from '@/types/message';
7
7
 
8
8
  describe('shareSlice actions', () => {
9
- let shareGPTServiceSpy: any;
9
+ let shareServiceSpy: any;
10
10
  let windowOpenSpy;
11
11
 
12
12
  beforeEach(() => {
13
- shareGPTServiceSpy = vi
14
- .spyOn(shareGPTService, 'createShareGPTUrl')
15
- .mockResolvedValue('test-url');
13
+ shareServiceSpy = vi.spyOn(shareService, 'createShareGPTUrl').mockResolvedValue('test-url');
16
14
  windowOpenSpy = vi.spyOn(window, 'open');
17
15
  });
18
16
 
@@ -23,7 +21,7 @@ describe('shareSlice actions', () => {
23
21
  describe('shareToShareGPT', () => {
24
22
  it('should share to ShareGPT and open a new window', async () => {
25
23
  const { result } = renderHook(() => useChatStore());
26
- const shareGPTServiceSpy = vi.spyOn(shareGPTService, 'createShareGPTUrl');
24
+ const shareServiceSpy = vi.spyOn(shareService, 'createShareGPTUrl');
27
25
  const windowOpenSpy = vi.spyOn(window, 'open');
28
26
  const avatar = 'avatar-url';
29
27
  const withPluginInfo = true;
@@ -33,7 +31,7 @@ describe('shareSlice actions', () => {
33
31
  await result.current.shareToShareGPT({ avatar, withPluginInfo, withSystemRole });
34
32
  });
35
33
 
36
- expect(shareGPTServiceSpy).toHaveBeenCalled();
34
+ expect(shareServiceSpy).toHaveBeenCalled();
37
35
  expect(windowOpenSpy).toHaveBeenCalled();
38
36
  });
39
37
  it('should handle messages from different roles correctly', async () => {
@@ -67,7 +65,7 @@ describe('shareSlice actions', () => {
67
65
  await result.current.shareToShareGPT({});
68
66
  });
69
67
 
70
- expect(shareGPTServiceSpy).toHaveBeenCalledWith(
68
+ expect(shareServiceSpy).toHaveBeenCalledWith(
71
69
  expect.objectContaining({
72
70
  avatarUrl: DEFAULT_USER_AVATAR_URL,
73
71
  }),
@@ -106,7 +104,7 @@ describe('shareSlice actions', () => {
106
104
  await act(async () => {
107
105
  result.current.shareToShareGPT({ withPluginInfo: true });
108
106
  });
109
- expect(shareGPTServiceSpy).toHaveBeenCalledWith(
107
+ expect(shareServiceSpy).toHaveBeenCalledWith(
110
108
  expect.objectContaining({
111
109
  items: expect.arrayContaining([
112
110
  expect.objectContaining({
@@ -139,7 +137,7 @@ describe('shareSlice actions', () => {
139
137
  await act(async () => {
140
138
  result.current.shareToShareGPT({ withPluginInfo: false });
141
139
  });
142
- expect(shareGPTServiceSpy).toHaveBeenCalledWith(
140
+ expect(shareServiceSpy).toHaveBeenCalledWith(
143
141
  expect.objectContaining({
144
142
  items: expect.not.arrayContaining([
145
143
  expect.objectContaining({
@@ -180,7 +178,7 @@ describe('shareSlice actions', () => {
180
178
  });
181
179
  });
182
180
 
183
- expect(shareGPTServiceSpy).toHaveBeenCalledWith(
181
+ expect(shareServiceSpy).toHaveBeenCalledWith(
184
182
  expect.objectContaining({
185
183
  items: [
186
184
  expect.objectContaining({ from: 'gpt' }), // Agent meta info
@@ -3,7 +3,7 @@ import { produce } from 'immer';
3
3
  import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
6
- import { shareGPTService } from '@/services/share';
6
+ import { shareService } from '@/services/share';
7
7
  import { useAgentStore } from '@/store/agent';
8
8
  import { agentSelectors } from '@/store/agent/selectors';
9
9
  import { useSessionStore } from '@/store/session';
@@ -104,7 +104,7 @@ export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [
104
104
 
105
105
  set({ shareLoading: true });
106
106
 
107
- const res = await shareGPTService.createShareGPTUrl({
107
+ const res = await shareService.createShareGPTUrl({
108
108
  avatarUrl: avatar || DEFAULT_USER_AVATAR_URL,
109
109
  items: shareMsgs,
110
110
  });
@@ -4,6 +4,7 @@ import { withSWR } from '~test-utils';
4
4
 
5
5
  import { globalService } from '@/services/global';
6
6
  import { useGlobalStore } from '@/store/global/index';
7
+ import { initialState } from '@/store/global/initialState';
7
8
 
8
9
  vi.mock('zustand/traditional');
9
10
 
@@ -141,4 +142,41 @@ describe('createPreferenceSlice', () => {
141
142
  expect(useGlobalStore.getState().latestVersion).toBe(latestVersion);
142
143
  });
143
144
  });
145
+
146
+ describe('useInitGlobalPreference', () => {
147
+ it('should init global preference if there is empty object', async () => {
148
+ vi.spyOn(
149
+ useGlobalStore.getState().preferenceStorage,
150
+ 'getFromLocalStorage',
151
+ ).mockReturnValueOnce({} as any);
152
+
153
+ const { result } = renderHook(() => useGlobalStore().useInitGlobalPreference(), {
154
+ wrapper: withSWR,
155
+ });
156
+
157
+ await waitFor(() => {
158
+ expect(result.current.data).toEqual({});
159
+ });
160
+
161
+ expect(useGlobalStore.getState().preference).toEqual(initialState.preference);
162
+ });
163
+
164
+ it('should update with data', async () => {
165
+ const { result } = renderHook(() => useGlobalStore());
166
+ vi.spyOn(
167
+ useGlobalStore.getState().preferenceStorage,
168
+ 'getFromLocalStorage',
169
+ ).mockReturnValueOnce({ inputHeight: 300 } as any);
170
+
171
+ const { result: hooks } = renderHook(() => result.current.useInitGlobalPreference(), {
172
+ wrapper: withSWR,
173
+ });
174
+
175
+ await waitFor(() => {
176
+ expect(hooks.current.data).toEqual({ inputHeight: 300 });
177
+ });
178
+
179
+ expect(result.current.preference.inputHeight).toEqual(300);
180
+ });
181
+ });
144
182
  });
@@ -1,3 +1,4 @@
1
+ import isEqual from 'fast-deep-equal';
1
2
  import { produce } from 'immer';
2
3
  import { gt } from 'semver';
3
4
  import useSWR, { SWRResponse } from 'swr';
@@ -94,9 +95,11 @@ export const globalActionSlice: StateCreator<
94
95
  () => get().preferenceStorage.getFromLocalStorage(),
95
96
  {
96
97
  onSuccess: (preference) => {
97
- if (preference) {
98
- set({ preference }, false, n('initPreference'));
99
- }
98
+ const nextPreference = merge(get().preference, preference);
99
+
100
+ if (isEqual(get().preference, nextPreference)) return;
101
+
102
+ set({ preference: nextPreference }, false, n('initPreference'));
100
103
  },
101
104
  },
102
105
  ),
@@ -1,6 +1,8 @@
1
- import { act, renderHook } from '@testing-library/react';
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { withSWR } from '~test-utils';
3
4
 
5
+ import { globalService } from '@/services/global';
4
6
  import { useUserStore } from '@/store/user';
5
7
 
6
8
  import { type Guide } from './initialState';
@@ -38,4 +40,23 @@ describe('createPreferenceSlice', () => {
38
40
  expect(result.current.preference.hideSyncAlert).toEqual(true);
39
41
  });
40
42
  });
43
+
44
+ describe('useInitPreference', () => {
45
+ it('should return false when userId is empty', async () => {
46
+ const { result } = renderHook(() => useUserStore());
47
+
48
+ vi.spyOn(result.current.preferenceStorage, 'getFromLocalStorage').mockResolvedValueOnce(
49
+ {} as any,
50
+ );
51
+
52
+ const { result: prefernce } = renderHook(() => result.current.useInitPreference(), {
53
+ wrapper: withSWR,
54
+ });
55
+
56
+ await waitFor(() => {
57
+ expect(prefernce.current.data).toEqual({});
58
+ expect(result.current.isPreferenceInit).toBeTruthy();
59
+ });
60
+ });
61
+ });
41
62
  });
@@ -41,9 +41,7 @@ export const createPreferenceSlice: StateCreator<
41
41
  () => get().preferenceStorage.getFromLocalStorage(),
42
42
  {
43
43
  onSuccess: (preference) => {
44
- if (preference) {
45
- set({ preference }, false, n('initPreference'));
46
- }
44
+ set({ isPreferenceInit: true, preference }, false, n('initPreference'));
47
45
  },
48
46
  },
49
47
  ),
@@ -16,6 +16,7 @@ export interface UserPreference {
16
16
  }
17
17
 
18
18
  export interface UserPreferenceState {
19
+ isPreferenceInit: boolean;
19
20
  /**
20
21
  * the user preference, which only store in local storage
21
22
  */
@@ -24,6 +25,7 @@ export interface UserPreferenceState {
24
25
  }
25
26
 
26
27
  export const initialPreferenceState: UserPreferenceState = {
28
+ isPreferenceInit: false,
27
29
  preference: {
28
30
  guide: {},
29
31
  telemetry: null,
@@ -5,9 +5,11 @@ const useCmdEnterToSend = (s: UserStore): boolean => s.preference.useCmdEnterToS
5
5
  const userAllowTrace = (s: UserStore) => s.preference.telemetry;
6
6
 
7
7
  const hideSyncAlert = (s: UserStore) => s.preference.hideSyncAlert;
8
+ const isPreferenceInit = (s: UserStore) => s.isPreferenceInit;
8
9
 
9
10
  export const preferenceSelectors = {
10
11
  hideSyncAlert,
12
+ isPreferenceInit,
11
13
  useCmdEnterToSend,
12
14
  userAllowTrace,
13
15
  };
@@ -2,17 +2,11 @@ import { act, renderHook } from '@testing-library/react';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import { userService } from '@/services/user';
5
- import { UserStore, useUserStore } from '@/store/user';
6
- import { UserSettingsState, initialSettingsState } from '@/store/user/slices/settings/initialState';
7
- import {
8
- modelConfigSelectors,
9
- modelProviderSelectors,
10
- settingsSelectors,
11
- } from '@/store/user/slices/settings/selectors';
5
+ import { useUserStore } from '@/store/user';
12
6
  import { GeneralModelProviderConfig } from '@/types/settings';
13
- import { merge } from '@/utils/merge';
14
7
 
15
- import { CustomModelCardDispatch, customModelCardsReducer } from '../reducers/customModelCard';
8
+ import { CustomModelCardDispatch } from '../reducers/customModelCard';
9
+ import { modelProviderSelectors, settingsSelectors } from '../selectors';
16
10
 
17
11
  // Mock userService
18
12
  vi.mock('@/services/user', () => ({
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_LANG } from '@/const/locale';
2
2
  import { DEFAULT_AGENT_META } from '@/const/meta';
3
- import { DEFAULT_AGENT, DEFAULT_TTS_CONFIG } from '@/const/settings';
3
+ import { DEFAULT_AGENT, DEFAULT_AGENT_CONFIG, DEFAULT_TTS_CONFIG } from '@/const/settings';
4
4
  import { Locales } from '@/locales/resources';
5
5
  import { GeneralModelProviderConfig, GlobalLLMProviderKey, GlobalSettings } from '@/types/settings';
6
6
  import { isOnServerSide } from '@/utils/env';
@@ -21,6 +21,7 @@ const password = (s: UserStore) => currentSettings(s).password;
21
21
  const currentTTS = (s: UserStore) => merge(DEFAULT_TTS_CONFIG, currentSettings(s).tts);
22
22
 
23
23
  const defaultAgent = (s: UserStore) => merge(DEFAULT_AGENT, currentSettings(s).defaultAgent);
24
+ const defaultAgentConfig = (s: UserStore) => merge(DEFAULT_AGENT_CONFIG, defaultAgent(s).config);
24
25
 
25
26
  const defaultAgentMeta = (s: UserStore) => merge(DEFAULT_AGENT_META, defaultAgent(s).meta);
26
27
 
@@ -53,6 +54,7 @@ export const settingsSelectors = {
53
54
  currentTTS,
54
55
  dalleConfig,
55
56
  defaultAgent,
57
+ defaultAgentConfig,
56
58
  defaultAgentMeta,
57
59
  exportSettings,
58
60
  isDalleAutoGenerating,