@lobehub/chat 0.140.1 → 0.141.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/locales/ar/common.json +34 -6
  3. package/locales/ar/setting.json +36 -0
  4. package/locales/de-DE/common.json +34 -6
  5. package/locales/de-DE/setting.json +36 -0
  6. package/locales/en-US/common.json +34 -6
  7. package/locales/en-US/setting.json +36 -0
  8. package/locales/es-ES/common.json +34 -6
  9. package/locales/es-ES/setting.json +36 -0
  10. package/locales/fr-FR/common.json +34 -6
  11. package/locales/fr-FR/setting.json +36 -0
  12. package/locales/it-IT/common.json +34 -6
  13. package/locales/it-IT/setting.json +38 -0
  14. package/locales/ja-JP/common.json +34 -6
  15. package/locales/ja-JP/setting.json +38 -0
  16. package/locales/ko-KR/common.json +34 -6
  17. package/locales/ko-KR/setting.json +36 -0
  18. package/locales/nl-NL/common.json +34 -6
  19. package/locales/nl-NL/setting.json +38 -0
  20. package/locales/pl-PL/common.json +34 -6
  21. package/locales/pl-PL/setting.json +36 -0
  22. package/locales/pt-BR/common.json +34 -6
  23. package/locales/pt-BR/setting.json +36 -0
  24. package/locales/ru-RU/common.json +34 -6
  25. package/locales/ru-RU/setting.json +36 -0
  26. package/locales/tr-TR/common.json +34 -6
  27. package/locales/tr-TR/setting.json +36 -0
  28. package/locales/vi-VN/common.json +34 -6
  29. package/locales/vi-VN/setting.json +36 -0
  30. package/locales/zh-CN/common.json +34 -6
  31. package/locales/zh-CN/setting.json +36 -0
  32. package/locales/zh-TW/common.json +34 -6
  33. package/locales/zh-TW/setting.json +36 -0
  34. package/package.json +10 -5
  35. package/src/app/chat/(desktop)/features/SessionHeader.tsx +5 -1
  36. package/src/app/chat/(mobile)/features/SessionHeader.tsx +9 -4
  37. package/src/app/chat/features/SessionListContent/List/SkeletonList.tsx +0 -1
  38. package/src/app/settings/(desktop)/features/Header.tsx +11 -1
  39. package/src/app/settings/(mobile)/features/Header/index.tsx +12 -1
  40. package/src/app/settings/features/SettingList/index.tsx +2 -1
  41. package/src/app/settings/sync/Alert.tsx +39 -0
  42. package/src/app/settings/sync/DeviceInfo/Card.tsx +41 -0
  43. package/src/app/settings/sync/DeviceInfo/DeviceName.tsx +66 -0
  44. package/src/app/settings/sync/DeviceInfo/index.tsx +117 -0
  45. package/src/app/settings/sync/PageTitle.tsx +11 -0
  46. package/src/app/settings/sync/WebRTC/ChannelNameInput.tsx +46 -0
  47. package/src/app/settings/sync/WebRTC/index.tsx +97 -0
  48. package/src/app/settings/sync/components/SyncSwitch/index.css +237 -0
  49. package/src/app/settings/sync/components/SyncSwitch/index.tsx +79 -0
  50. package/src/app/settings/sync/components/SystemIcon.tsx +16 -0
  51. package/src/app/settings/sync/layout.tsx +9 -0
  52. package/src/app/settings/sync/page.tsx +23 -0
  53. package/src/app/settings/sync/util.ts +4 -0
  54. package/src/components/BrowserIcon/components/Brave.tsx +56 -0
  55. package/src/components/BrowserIcon/components/Chrome.tsx +14 -0
  56. package/src/components/BrowserIcon/components/Chromium.tsx +14 -0
  57. package/src/components/BrowserIcon/components/Edge.tsx +36 -0
  58. package/src/components/BrowserIcon/components/Firefox.tsx +38 -0
  59. package/src/components/BrowserIcon/components/Opera.tsx +19 -0
  60. package/src/components/BrowserIcon/components/Safari.tsx +23 -0
  61. package/src/components/BrowserIcon/components/Samsung.tsx +21 -0
  62. package/src/components/BrowserIcon/index.tsx +50 -0
  63. package/src/components/BrowserIcon/types.ts +8 -0
  64. package/src/const/settings.ts +6 -0
  65. package/src/database/core/__tests__/model.test.ts +2 -2
  66. package/src/database/core/db.ts +1 -1
  67. package/src/database/core/index.ts +1 -0
  68. package/src/database/core/model.ts +83 -5
  69. package/src/database/core/sync.ts +328 -0
  70. package/src/database/models/__tests__/message.test.ts +0 -1
  71. package/src/database/models/__tests__/plugin.test.ts +5 -2
  72. package/src/database/models/file.ts +1 -1
  73. package/src/database/models/message.ts +49 -30
  74. package/src/database/models/plugin.ts +6 -5
  75. package/src/database/models/session.ts +15 -16
  76. package/src/database/models/sessionGroup.ts +14 -8
  77. package/src/database/models/topic.ts +14 -21
  78. package/src/features/SyncStatusInspector/DisableSync.tsx +79 -0
  79. package/src/features/SyncStatusInspector/EnableSync.tsx +136 -0
  80. package/src/features/SyncStatusInspector/EnableTag.tsx +66 -0
  81. package/src/features/SyncStatusInspector/index.tsx +27 -0
  82. package/src/hooks/useSyncData.ts +48 -0
  83. package/src/layout/GlobalLayout/StoreHydration.tsx +5 -0
  84. package/src/locales/default/common.ts +27 -5
  85. package/src/locales/default/setting.ts +37 -1
  86. package/src/services/chat.ts +6 -2
  87. package/src/services/config.ts +1 -1
  88. package/src/services/global.ts +15 -0
  89. package/src/store/chat/slices/topic/action.test.ts +1 -1
  90. package/src/store/chat/slices/topic/action.ts +21 -10
  91. package/src/store/global/slices/common/action.ts +71 -1
  92. package/src/store/global/slices/common/initialState.ts +9 -0
  93. package/src/store/global/slices/common/selectors.ts +1 -0
  94. package/src/store/global/slices/preference/initialState.ts +2 -1
  95. package/src/store/global/slices/preference/selectors.ts +3 -0
  96. package/src/store/global/slices/settings/selectors/index.ts +1 -0
  97. package/src/store/global/slices/settings/selectors/sync.ts +14 -0
  98. package/src/types/settings/index.ts +3 -0
  99. package/src/types/settings/sync.ts +10 -0
  100. package/src/types/sync.ts +41 -0
  101. package/src/utils/platform.ts +9 -3
  102. package/src/utils/responsive.ts +21 -0
@@ -9,6 +9,7 @@ import { StateCreator } from 'zustand/vanilla';
9
9
  import { chainSummaryTitle } from '@/chains/summaryTitle';
10
10
  import { LOADING_FLAT } from '@/const/message';
11
11
  import { TraceNameMap } from '@/const/trace';
12
+ import { useClientDataSWR } from '@/libs/swr';
12
13
  import { chatService } from '@/services/chat';
13
14
  import { messageService } from '@/services/message';
14
15
  import { topicService } from '@/services/topic';
@@ -22,6 +23,9 @@ import { topicSelectors } from './selectors';
22
23
 
23
24
  const n = setNamespace('topic');
24
25
 
26
+ const SWR_USE_FETCH_TOPIC = 'SWR_USE_FETCH_TOPIC';
27
+ const SWR_USE_SEARCH_TOPIC = 'SWR_USE_SEARCH_TOPIC';
28
+
25
29
  export interface ChatTopicAction {
26
30
  favoriteTopic: (id: string, favState: boolean) => Promise<void>;
27
31
  openNewTopicOrSaveTopic: () => Promise<void>;
@@ -141,18 +145,25 @@ export const chatTopic: StateCreator<
141
145
  },
142
146
  // query
143
147
  useFetchTopics: (sessionId) =>
144
- useSWR<ChatTopic[]>(sessionId, async (sessionId) => topicService.getTopics({ sessionId }), {
145
- onSuccess: (topics) => {
146
- set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId }));
148
+ useClientDataSWR<ChatTopic[]>(
149
+ [SWR_USE_FETCH_TOPIC, sessionId],
150
+ async ([, sessionId]: [string, string]) => topicService.getTopics({ sessionId }),
151
+ {
152
+ onSuccess: (topics) => {
153
+ set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId }));
154
+ },
147
155
  },
148
- dedupingInterval: 0,
149
- }),
156
+ ),
150
157
  useSearchTopics: (keywords) =>
151
- useSWR<ChatTopic[]>(keywords, topicService.searchTopics, {
152
- onSuccess: (data) => {
153
- set({ searchTopics: data }, false, n('useSearchTopics(success)', { keywords }));
158
+ useSWR<ChatTopic[]>(
159
+ [SWR_USE_SEARCH_TOPIC, keywords],
160
+ ([, keywords]: [string, string]) => topicService.searchTopics(keywords),
161
+ {
162
+ onSuccess: (data) => {
163
+ set({ searchTopics: data }, false, n('useSearchTopics(success)', { keywords }));
164
+ },
154
165
  },
155
- }),
166
+ ),
156
167
  switchTopic: async (id) => {
157
168
  set({ activeTopicId: id }, false, n('toggleTopic'));
158
169
 
@@ -213,6 +224,6 @@ export const chatTopic: StateCreator<
213
224
  set({ topicLoadingId: id }, false, n('updateTopicLoading'));
214
225
  },
215
226
  refreshTopic: async () => {
216
- await mutate(get().activeId);
227
+ await mutate([SWR_USE_FETCH_TOPIC, get().activeId]);
217
228
  },
218
229
  });
@@ -11,12 +11,15 @@ import { messageService } from '@/services/message';
11
11
  import { UserConfig, userService } from '@/services/user';
12
12
  import type { GlobalStore } from '@/store/global';
13
13
  import type { GlobalServerConfig, GlobalSettings } from '@/types/settings';
14
+ import { OnSyncEvent, PeerSyncStatus } from '@/types/sync';
14
15
  import { merge } from '@/utils/merge';
16
+ import { browserInfo } from '@/utils/platform';
15
17
  import { setNamespace } from '@/utils/storeDebug';
16
18
  import { switchLang } from '@/utils/switchLang';
17
19
 
18
20
  import { preferenceSelectors } from '../preference/selectors';
19
- import { settingsSelectors } from '../settings/selectors';
21
+ import { settingsSelectors, syncSettingsSelectors } from '../settings/selectors';
22
+ import { commonSelectors } from './selectors';
20
23
 
21
24
  const n = setNamespace('common');
22
25
 
@@ -24,11 +27,18 @@ const n = setNamespace('common');
24
27
  * 设置操作
25
28
  */
26
29
  export interface CommonAction {
30
+ refreshConnection: (onEvent: OnSyncEvent) => Promise<void>;
27
31
  refreshUserConfig: () => Promise<void>;
28
32
  switchBackToChat: (sessionId?: string) => void;
33
+ triggerEnableSync: (userId: string, onEvent: OnSyncEvent) => Promise<boolean>;
29
34
  updateAvatar: (avatar: string) => Promise<void>;
30
35
  useCheckLatestVersion: () => SWRResponse<string>;
31
36
  useCheckTrace: (shouldFetch: boolean) => SWRResponse;
37
+ useEnabledSync: (
38
+ userEnableSync: boolean,
39
+ userId: string | undefined,
40
+ onEvent: OnSyncEvent,
41
+ ) => SWRResponse;
32
42
  useFetchServerConfig: () => SWRResponse;
33
43
  useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
34
44
  }
@@ -41,13 +51,53 @@ export const createCommonSlice: StateCreator<
41
51
  [],
42
52
  CommonAction
43
53
  > = (set, get) => ({
54
+ refreshConnection: async (onEvent) => {
55
+ const userId = commonSelectors.userId(get());
56
+
57
+ if (!userId) return;
58
+
59
+ await get().triggerEnableSync(userId, onEvent);
60
+ },
61
+
44
62
  refreshUserConfig: async () => {
45
63
  await mutate([USER_CONFIG_FETCH_KEY, true]);
46
64
  },
65
+
47
66
  switchBackToChat: (sessionId) => {
48
67
  get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile));
49
68
  },
69
+ triggerEnableSync: async (userId: string, onEvent: OnSyncEvent) => {
70
+ // double-check the sync ability
71
+ // if there is no channelName, don't start sync
72
+ const sync = syncSettingsSelectors.webrtcConfig(get());
73
+ if (!sync.channelName) return false;
74
+
75
+ const name = syncSettingsSelectors.deviceName(get());
50
76
 
77
+ const defaultUserName = `My ${browserInfo.browser} (${browserInfo.os})`;
78
+
79
+ set({ syncStatus: PeerSyncStatus.Connecting });
80
+ return globalService.enabledSync({
81
+ channel: {
82
+ name: sync.channelName,
83
+ password: sync.channelPassword,
84
+ },
85
+ onAwarenessChange(state) {
86
+ set({ syncAwareness: state });
87
+ },
88
+ onSyncEvent: onEvent,
89
+ onSyncStatusChange: (status) => {
90
+ set({ syncStatus: status });
91
+ },
92
+ signaling: sync.signaling,
93
+ user: {
94
+ id: userId,
95
+ // if user don't set the name, use default name
96
+ name: name || defaultUserName,
97
+ ...browserInfo,
98
+ },
99
+ });
100
+ },
51
101
  updateAvatar: async (avatar) => {
52
102
  await userService.updateAvatar(avatar);
53
103
  await get().refreshUserConfig();
@@ -78,6 +128,26 @@ export const createCommonSlice: StateCreator<
78
128
  revalidateOnFocus: false,
79
129
  },
80
130
  ),
131
+
132
+ useEnabledSync: (userEnableSync, userId, onEvent) =>
133
+ useSWR<boolean>(
134
+ ['enableSync', userEnableSync, userId],
135
+ async () => {
136
+ // if user don't enable sync or no userId ,don't start sync
137
+ if (!userId) return false;
138
+
139
+ // if user don't enable sync, stop sync
140
+ if (!userEnableSync) return globalService.disableSync();
141
+
142
+ return get().triggerEnableSync(userId, onEvent);
143
+ },
144
+ {
145
+ onSuccess: (syncEnabled) => {
146
+ set({ syncEnabled });
147
+ },
148
+ revalidateOnFocus: false,
149
+ },
150
+ ),
81
151
  useFetchServerConfig: () =>
82
152
  useSWR<GlobalServerConfig>('fetchGlobalConfig', globalService.getGlobalConfig, {
83
153
  onSuccess: (data) => {
@@ -1,5 +1,7 @@
1
1
  import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
2
2
 
3
+ import { PeerSyncStatus, SyncAwarenessState } from '@/types/sync';
4
+
3
5
  export enum SidebarTabKey {
4
6
  Chat = 'chat',
5
7
  Market = 'market',
@@ -11,6 +13,7 @@ export enum SettingsTabs {
11
13
  Agent = 'agent',
12
14
  Common = 'common',
13
15
  LLM = 'llm',
16
+ Sync = 'sync',
14
17
  TTS = 'tts',
15
18
  }
16
19
 
@@ -25,9 +28,15 @@ export interface GlobalCommonState {
25
28
  latestVersion?: string;
26
29
  router?: AppRouterInstance;
27
30
  sidebarKey: SidebarTabKey;
31
+ syncAwareness: SyncAwarenessState[];
32
+ syncEnabled: boolean;
33
+ syncStatus: PeerSyncStatus;
28
34
  }
29
35
 
30
36
  export const initialCommonState: GlobalCommonState = {
31
37
  isMobile: false,
32
38
  sidebarKey: SidebarTabKey.Chat,
39
+ syncAwareness: [],
40
+ syncEnabled: false,
41
+ syncStatus: PeerSyncStatus.Disabled,
33
42
  };
@@ -4,4 +4,5 @@ export const commonSelectors = {
4
4
  enabledOAuthSSO: (s: GlobalStore) => s.serverConfig.enabledOAuthSSO,
5
5
  enabledTelemetryChat: (s: GlobalStore) => s.serverConfig.telemetry.langfuse || false,
6
6
  userAvatar: (s: GlobalStore) => s.avatar || '',
7
+ userId: (s: GlobalStore) => s.userId,
7
8
  };
@@ -9,10 +9,11 @@ export interface GlobalPreference {
9
9
  // which sessionGroup should expand
10
10
  expandSessionGroupKeys: SessionGroupId[];
11
11
  guide?: Guide;
12
+ hideSyncAlert?: boolean;
12
13
  inputHeight: number;
13
14
  mobileShowTopic?: boolean;
14
- sessionsWidth: number;
15
15
 
16
+ sessionsWidth: number;
16
17
  showChatSideBar?: boolean;
17
18
  showSessionPanel?: boolean;
18
19
  showSystemRole?: boolean;
@@ -6,7 +6,10 @@ const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterT
6
6
 
7
7
  const userAllowTrace = (s: GlobalStore) => s.preference.telemetry;
8
8
 
9
+ const hideSyncAlert = (s: GlobalStore) => s.preference.hideSyncAlert;
10
+
9
11
  export const preferenceSelectors = {
12
+ hideSyncAlert,
10
13
  sessionGroupKeys,
11
14
  useCmdEnterToSend,
12
15
  userAllowTrace,
@@ -1,2 +1,3 @@
1
1
  export { modelProviderSelectors } from './modelProvider';
2
2
  export { settingsSelectors } from './settings';
3
+ export { syncSettingsSelectors } from './sync';
@@ -0,0 +1,14 @@
1
+ import { GlobalStore } from '../../../store';
2
+ import { currentSettings } from './settings';
3
+
4
+ const webrtcConfig = (s: GlobalStore) => currentSettings(s).sync.webrtc;
5
+ const webrtcChannelName = (s: GlobalStore) => webrtcConfig(s).channelName;
6
+ const enableWebRTC = (s: GlobalStore) => webrtcConfig(s).enabled;
7
+ const deviceName = (s: GlobalStore) => currentSettings(s).sync.deviceName;
8
+
9
+ export const syncSettingsSelectors = {
10
+ deviceName,
11
+ enableWebRTC,
12
+ webrtcChannelName,
13
+ webrtcConfig,
14
+ };
@@ -4,12 +4,14 @@ import type { LobeAgentSession } from '@/types/session';
4
4
 
5
5
  import { GlobalBaseSettings } from './base';
6
6
  import { GlobalLLMConfig } from './modelProvider';
7
+ import { GlobalSyncSettings } from './sync';
7
8
  import { GlobalTTSConfig } from './tts';
8
9
 
9
10
  export type GlobalDefaultAgent = Pick<LobeAgentSession, 'config' | 'meta'>;
10
11
 
11
12
  export * from './base';
12
13
  export * from './modelProvider';
14
+ export * from './sync';
13
15
  export * from './tts';
14
16
 
15
17
  export interface GlobalTool {
@@ -34,6 +36,7 @@ export interface GlobalServerConfig {
34
36
  export interface GlobalSettings extends GlobalBaseSettings {
35
37
  defaultAgent: GlobalDefaultAgent;
36
38
  languageModel: GlobalLLMConfig;
39
+ sync: GlobalSyncSettings;
37
40
  tool: GlobalTool;
38
41
  tts: GlobalTTSConfig;
39
42
  }
@@ -0,0 +1,10 @@
1
+ export interface WebRTCSyncConfig {
2
+ channelName?: string;
3
+ channelPassword?: string;
4
+ enabled: boolean;
5
+ signaling?: string;
6
+ }
7
+ export interface GlobalSyncSettings {
8
+ deviceName?: string;
9
+ webrtc: WebRTCSyncConfig;
10
+ }
@@ -0,0 +1,41 @@
1
+ import { LobeDBSchemaMap } from '@/database/core/db';
2
+
3
+ export type OnSyncEvent = (tableKey: keyof LobeDBSchemaMap) => void;
4
+ export type OnSyncStatusChange = (status: PeerSyncStatus) => void;
5
+ export type OnAwarenessChange = (state: SyncAwarenessState[]) => void;
6
+
7
+ // export type PeerSyncStatus = 'syncing' | 'synced' | 'ready' | 'unconnected';
8
+
9
+ export enum PeerSyncStatus {
10
+ Connecting = 'connecting',
11
+ Disabled = 'disabled',
12
+ Ready = 'ready',
13
+ Synced = 'synced',
14
+ Syncing = 'syncing',
15
+ Unconnected = 'unconnected',
16
+ }
17
+
18
+ export interface StartDataSyncParams {
19
+ channel: {
20
+ name: string;
21
+ password?: string;
22
+ };
23
+ onAwarenessChange: OnAwarenessChange;
24
+ onSyncEvent: OnSyncEvent;
25
+ onSyncStatusChange: OnSyncStatusChange;
26
+ signaling?: string;
27
+ user: SyncUserInfo;
28
+ }
29
+
30
+ export interface SyncUserInfo {
31
+ browser?: string;
32
+ id: string;
33
+ isMobile: boolean;
34
+ name?: string;
35
+ os?: string;
36
+ }
37
+
38
+ export interface SyncAwarenessState extends SyncUserInfo {
39
+ clientID: number;
40
+ current: boolean;
41
+ }
@@ -1,6 +1,6 @@
1
1
  import UAParser from 'ua-parser-js';
2
2
 
3
- const getPaser = () => {
3
+ const getParser = () => {
4
4
  if (typeof window === 'undefined') return new UAParser('Node');
5
5
 
6
6
  let ua = navigator.userAgent;
@@ -8,11 +8,17 @@ const getPaser = () => {
8
8
  };
9
9
 
10
10
  export const getPlatform = () => {
11
- return getPaser().getOS().name;
11
+ return getParser().getOS().name;
12
12
  };
13
13
 
14
14
  export const getBrowser = () => {
15
- return getPaser().getResult().browser.name;
15
+ return getParser().getResult().browser.name;
16
+ };
17
+
18
+ export const browserInfo = {
19
+ browser: getBrowser(),
20
+ isMobile: getParser().getDevice().type === 'mobile',
21
+ os: getParser().getOS().name,
16
22
  };
17
23
 
18
24
  export const isMacOS = () => getPlatform() === 'Mac OS';
@@ -17,3 +17,24 @@ export const isMobileDevice = () => {
17
17
 
18
18
  return device.type === 'mobile';
19
19
  };
20
+
21
+ /**
22
+ * check mobile device in server
23
+ */
24
+ export const gerServerDeviceInfo = () => {
25
+ if (typeof process === 'undefined') {
26
+ throw new Error('[Server method] you are importing a server-only module outside of server');
27
+ }
28
+
29
+ const { get } = headers();
30
+ const ua = get('user-agent');
31
+
32
+ // console.debug(ua);
33
+ const parser = new UAParser(ua || '');
34
+
35
+ return {
36
+ browser: parser.getBrowser().name,
37
+ isMobile: isMobileDevice(),
38
+ os: parser.getOS().name,
39
+ };
40
+ };