@lobehub/lobehub 2.0.0-next.240 → 2.0.0-next.241

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 (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/resources/locales/ar/menu.json +5 -1
  3. package/apps/desktop/resources/locales/bg-BG/menu.json +5 -1
  4. package/apps/desktop/resources/locales/de-DE/menu.json +5 -1
  5. package/apps/desktop/resources/locales/es-ES/menu.json +5 -1
  6. package/apps/desktop/resources/locales/fa-IR/menu.json +5 -1
  7. package/apps/desktop/resources/locales/fr-FR/menu.json +5 -1
  8. package/apps/desktop/resources/locales/it-IT/menu.json +5 -1
  9. package/apps/desktop/resources/locales/ja-JP/menu.json +5 -1
  10. package/apps/desktop/resources/locales/ko-KR/menu.json +5 -1
  11. package/apps/desktop/resources/locales/nl-NL/menu.json +5 -1
  12. package/apps/desktop/resources/locales/pl-PL/menu.json +5 -1
  13. package/apps/desktop/resources/locales/pt-BR/menu.json +5 -1
  14. package/apps/desktop/resources/locales/ru-RU/menu.json +5 -1
  15. package/apps/desktop/resources/locales/tr-TR/menu.json +5 -1
  16. package/apps/desktop/resources/locales/vi-VN/menu.json +5 -1
  17. package/apps/desktop/resources/locales/zh-CN/menu.json +5 -1
  18. package/apps/desktop/resources/locales/zh-TW/menu.json +5 -1
  19. package/apps/desktop/src/main/locales/default/menu.ts +5 -1
  20. package/apps/desktop/src/main/menus/impls/linux.ts +30 -0
  21. package/apps/desktop/src/main/menus/impls/macOS.test.ts +17 -0
  22. package/apps/desktop/src/main/menus/impls/macOS.ts +33 -0
  23. package/apps/desktop/src/main/menus/impls/windows.ts +30 -0
  24. package/changelog/v1.json +5 -0
  25. package/locales/ar/electron.json +24 -0
  26. package/locales/bg-BG/electron.json +24 -0
  27. package/locales/de-DE/electron.json +24 -0
  28. package/locales/en-US/electron.json +24 -0
  29. package/locales/es-ES/electron.json +24 -0
  30. package/locales/fa-IR/electron.json +24 -0
  31. package/locales/fr-FR/electron.json +24 -0
  32. package/locales/it-IT/electron.json +24 -0
  33. package/locales/ja-JP/electron.json +24 -0
  34. package/locales/ko-KR/electron.json +24 -0
  35. package/locales/nl-NL/electron.json +24 -0
  36. package/locales/pl-PL/electron.json +24 -0
  37. package/locales/pt-BR/electron.json +24 -0
  38. package/locales/ru-RU/electron.json +24 -0
  39. package/locales/tr-TR/electron.json +24 -0
  40. package/locales/vi-VN/electron.json +24 -0
  41. package/locales/zh-CN/electron.json +24 -0
  42. package/locales/zh-TW/electron.json +24 -0
  43. package/package.json +1 -1
  44. package/packages/electron-client-ipc/src/events/navigation.ts +12 -0
  45. package/src/components/PageTitle/index.tsx +11 -1
  46. package/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx +137 -0
  47. package/src/features/ElectronTitlebar/NavigationBar/index.tsx +86 -0
  48. package/src/features/ElectronTitlebar/helpers/routeMetadata.ts +214 -0
  49. package/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts +152 -0
  50. package/src/features/ElectronTitlebar/index.tsx +13 -5
  51. package/src/features/NavHeader/index.tsx +4 -2
  52. package/src/features/NavPanel/components/NavPanelDraggable.tsx +174 -0
  53. package/src/features/NavPanel/hooks/useNavPanel.ts +11 -35
  54. package/src/features/NavPanel/index.tsx +2 -126
  55. package/src/hooks/useTypeScriptHappyCallback.ts +7 -0
  56. package/src/locales/default/electron.ts +24 -0
  57. package/src/store/electron/actions/navigationHistory.ts +247 -0
  58. package/src/store/electron/initialState.ts +7 -1
  59. package/src/store/electron/store.ts +9 -2
  60. package/src/store/global/selectors/systemStatus.ts +4 -1
@@ -1,8 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { DraggablePanel } from '@lobehub/ui';
4
- import { createStaticStyles, cssVar } from 'antd-style';
5
- import { AnimatePresence, motion } from 'motion/react';
6
3
  import {
7
4
  type PropsWithChildren,
8
5
  type ReactNode,
@@ -11,14 +8,8 @@ import {
11
8
  useSyncExternalStore,
12
9
  } from 'react';
13
10
 
14
- import { USER_DROPDOWN_ICON_ID } from '@/app/[variants]/(main)/home/_layout/Header/components/User';
15
- import { isDesktop } from '@/const/version';
16
- import { TOGGLE_BUTTON_ID } from '@/features/NavPanel/ToggleLeftPanelButton';
17
- import { isMacOS } from '@/utils/platform';
18
-
19
11
  import Sidebar from '../../app/[variants]/(main)/home/_layout/Sidebar';
20
- import { BACK_BUTTON_ID } from './components/BackButton';
21
- import { useNavPanel } from './hooks/useNavPanel';
12
+ import { NavPanelDraggable } from './components/NavPanelDraggable';
22
13
 
23
14
  export const NAV_PANEL_RIGHT_DRAWER_ID = 'nav-panel-drawer';
24
15
 
@@ -41,82 +32,7 @@ const setNavPanelSnapshot = (snapshot: NavPanelSnapshot) => {
41
32
  listeners.forEach((listener) => listener());
42
33
  };
43
34
 
44
- export const styles = createStaticStyles(({ css, cssVar }) => ({
45
- content: css`
46
- position: relative;
47
-
48
- overflow: hidden;
49
- display: flex;
50
-
51
- height: 100%;
52
- min-height: 100%;
53
- max-height: 100%;
54
- `,
55
- inner: css`
56
- position: relative;
57
- inset: 0;
58
-
59
- overflow: hidden;
60
- flex: 1;
61
- flex-direction: column;
62
-
63
- min-width: 240px;
64
- `,
65
- panel: css`
66
- user-select: none;
67
- height: 100%;
68
- color: ${cssVar.colorTextSecondary};
69
- background: ${isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout};
70
-
71
- * {
72
- user-select: none;
73
- }
74
-
75
- #${TOGGLE_BUTTON_ID} {
76
- width: 0 !important;
77
- opacity: 0;
78
- transition:
79
- opacity,
80
- width 0.2s ${cssVar.motionEaseOut};
81
- }
82
-
83
- #${USER_DROPDOWN_ICON_ID} {
84
- width: 0 !important;
85
- opacity: 0;
86
- transition:
87
- opacity,
88
- width 0.2s ${cssVar.motionEaseOut};
89
- }
90
-
91
- #${BACK_BUTTON_ID} {
92
- width: 0 !important;
93
- opacity: 0;
94
- transition: all 0.2s ${cssVar.motionEaseOut};
95
- }
96
-
97
- &:hover {
98
- #${TOGGLE_BUTTON_ID} {
99
- width: 32px !important;
100
- opacity: 1;
101
- }
102
-
103
- #${USER_DROPDOWN_ICON_ID} {
104
- width: 14px !important;
105
- opacity: 1;
106
- }
107
-
108
- &:hover {
109
- #${BACK_BUTTON_ID} {
110
- width: 24px !important;
111
- opacity: 1;
112
- }
113
- }
114
- }
115
- `,
116
- }));
117
-
118
35
  const NavPanel = memo(() => {
119
- const { expand, handleSizeChange, width, togglePanel } = useNavPanel();
120
36
  const panelContent = useSyncExternalStore(
121
37
  subscribeNavPanel,
122
38
  getNavPanelSnapshot,
@@ -128,47 +44,7 @@ const NavPanel = memo(() => {
128
44
 
129
45
  return (
130
46
  <>
131
- <DraggablePanel
132
- className={styles.panel}
133
- classNames={{
134
- content: styles.content,
135
- }}
136
- defaultSize={{ height: '100%', width }}
137
- expand={expand}
138
- expandable={false}
139
- maxWidth={400}
140
- minWidth={240}
141
- onExpandChange={(expand) => togglePanel(expand)}
142
- onSizeChange={handleSizeChange}
143
- placement="left"
144
- showBorder={false}
145
- style={{
146
- background: isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout,
147
- zIndex: 11,
148
- }}
149
- >
150
- <AnimatePresence initial={false} mode="popLayout">
151
- <motion.div
152
- animate={{ opacity: 1, x: 0 }}
153
- className={styles.inner}
154
- exit={{
155
- opacity: 0,
156
- x: '-20%',
157
- }}
158
- initial={{
159
- opacity: 0,
160
- x: 0,
161
- }}
162
- key={activeContent.key}
163
- transition={{
164
- duration: 0.4,
165
- ease: [0.4, 0, 0.2, 1],
166
- }}
167
- >
168
- {activeContent.node}
169
- </motion.div>
170
- </AnimatePresence>
171
- </DraggablePanel>
47
+ <NavPanelDraggable activeContent={activeContent} />
172
48
  <div
173
49
  id={NAV_PANEL_RIGHT_DRAWER_ID}
174
50
  style={{
@@ -0,0 +1,7 @@
1
+ import type { DependencyList } from 'react';
2
+ import { useCallback } from 'react';
3
+
4
+ export const useTypeScriptHappyCallback: <Args extends unknown[], R>(
5
+ fn: (...args: Args) => R,
6
+ deps: DependencyList,
7
+ ) => (...args: Args) => R = useCallback;
@@ -1,4 +1,28 @@
1
1
  export default {
2
+ 'navigation.chat': 'Chat',
3
+ 'navigation.discover': 'Discover',
4
+ 'navigation.discoverAssistants': 'Discover Assistants',
5
+ 'navigation.discoverMcp': 'Discover MCP',
6
+ 'navigation.discoverModels': 'Discover Models',
7
+ 'navigation.discoverProviders': 'Discover Providers',
8
+ 'navigation.group': 'Group',
9
+ 'navigation.groupChat': 'Group Chat',
10
+ 'navigation.home': 'Home',
11
+ 'navigation.image': 'Image',
12
+ 'navigation.knowledgeBase': 'Knowledge Base',
13
+ 'navigation.lobehub': 'LobeHub',
14
+ 'navigation.memory': 'Memory',
15
+ 'navigation.memoryContexts': 'Memory - Contexts',
16
+ 'navigation.memoryExperiences': 'Memory - Experiences',
17
+ 'navigation.memoryIdentities': 'Memory - Identities',
18
+ 'navigation.memoryPreferences': 'Memory - Preferences',
19
+ 'navigation.onboarding': 'Onboarding',
20
+ 'navigation.page': 'Page',
21
+ 'navigation.pages': 'Pages',
22
+ 'navigation.provider': 'Provider',
23
+ 'navigation.recentView': 'Recent pages',
24
+ 'navigation.resources': 'Resources',
25
+ 'navigation.settings': 'Settings',
2
26
  'notification.finishChatGeneration': 'AI message generation completed',
3
27
  'proxy.auth': 'Authentication Required',
4
28
  'proxy.authDesc': 'If the proxy server requires a username and password',
@@ -0,0 +1,247 @@
1
+ import { type StateCreator } from 'zustand/vanilla';
2
+
3
+ import type { ElectronStore } from '../store';
4
+
5
+ // ======== Types ======== //
6
+
7
+ export interface HistoryEntry {
8
+ icon?: string;
9
+ metadata?: {
10
+ [key: string]: any;
11
+ sessionId?: string;
12
+ timestamp: number;
13
+ };
14
+ title: string;
15
+ url: string;
16
+ }
17
+
18
+ export interface NavigationHistoryState {
19
+ /**
20
+ * Current page title from PageTitle component
21
+ * Used to get dynamic titles without setTimeout hack
22
+ */
23
+ currentPageTitle: string;
24
+ /**
25
+ * Current position in history (-1 means empty)
26
+ */
27
+ historyCurrentIndex: number;
28
+ /**
29
+ * History entries list
30
+ */
31
+ historyEntries: HistoryEntry[];
32
+ /**
33
+ * Flag to indicate if currently navigating via back/forward
34
+ * Used to prevent adding duplicate history entries
35
+ */
36
+ isNavigatingHistory: boolean;
37
+ }
38
+
39
+ // ======== Action Interface ======== //
40
+
41
+ export interface NavigationHistoryAction {
42
+ /**
43
+ * Check if can go back in history
44
+ */
45
+ canGoBack: () => boolean;
46
+
47
+ /**
48
+ * Check if can go forward in history
49
+ */
50
+ canGoForward: () => boolean;
51
+
52
+ /**
53
+ * Get current history entry
54
+ */
55
+ getCurrentEntry: () => HistoryEntry | null;
56
+
57
+ /**
58
+ * Navigate back in history
59
+ * @returns The target entry or null if cannot go back
60
+ */
61
+ goBack: () => HistoryEntry | null;
62
+
63
+ /**
64
+ * Navigate forward in history
65
+ * @returns The target entry or null if cannot go forward
66
+ */
67
+ goForward: () => HistoryEntry | null;
68
+
69
+ /**
70
+ * Push a new entry to history (for normal navigation)
71
+ * Truncates any forward history if not at the end
72
+ */
73
+ pushHistory: (
74
+ entry: Omit<HistoryEntry, 'metadata'> & { metadata?: Partial<HistoryEntry['metadata']> },
75
+ ) => void;
76
+
77
+ /**
78
+ * Replace current entry in history (for replace navigation)
79
+ */
80
+ replaceHistory: (
81
+ entry: Omit<HistoryEntry, 'metadata'> & { metadata?: Partial<HistoryEntry['metadata']> },
82
+ ) => void;
83
+
84
+ /**
85
+ * Set current page title (called by PageTitle component)
86
+ */
87
+ setCurrentPageTitle: (title: string) => void;
88
+
89
+ /**
90
+ * Set the navigating history flag
91
+ */
92
+ setIsNavigatingHistory: (value: boolean) => void;
93
+ }
94
+
95
+ // ======== Initial State ======== //
96
+
97
+ export const navigationHistoryInitialState: NavigationHistoryState = {
98
+ currentPageTitle: '',
99
+ historyCurrentIndex: -1,
100
+ historyEntries: [],
101
+ isNavigatingHistory: false,
102
+ };
103
+
104
+ // ======== Action Implementation ======== //
105
+
106
+ export const createNavigationHistorySlice: StateCreator<
107
+ ElectronStore,
108
+ [['zustand/devtools', never]],
109
+ [],
110
+ NavigationHistoryAction
111
+ > = (set, get) => ({
112
+ canGoBack: () => {
113
+ const { historyCurrentIndex } = get();
114
+ return historyCurrentIndex > 0;
115
+ },
116
+
117
+ canGoForward: () => {
118
+ const { historyCurrentIndex, historyEntries } = get();
119
+ return historyCurrentIndex < historyEntries.length - 1;
120
+ },
121
+
122
+ getCurrentEntry: () => {
123
+ const { historyCurrentIndex, historyEntries } = get();
124
+ if (historyCurrentIndex < 0 || historyCurrentIndex >= historyEntries.length) {
125
+ return null;
126
+ }
127
+ return historyEntries[historyCurrentIndex];
128
+ },
129
+
130
+ goBack: () => {
131
+ const { historyCurrentIndex, historyEntries } = get();
132
+
133
+ if (historyCurrentIndex <= 0) {
134
+ return null;
135
+ }
136
+
137
+ const newIndex = historyCurrentIndex - 1;
138
+ const targetEntry = historyEntries[newIndex];
139
+
140
+ set(
141
+ {
142
+ historyCurrentIndex: newIndex,
143
+ isNavigatingHistory: true,
144
+ },
145
+ false,
146
+ 'goBack',
147
+ );
148
+
149
+ return targetEntry;
150
+ },
151
+
152
+ goForward: () => {
153
+ const { historyCurrentIndex, historyEntries } = get();
154
+
155
+ if (historyCurrentIndex >= historyEntries.length - 1) {
156
+ return null;
157
+ }
158
+
159
+ const newIndex = historyCurrentIndex + 1;
160
+ const targetEntry = historyEntries[newIndex];
161
+
162
+ set(
163
+ {
164
+ historyCurrentIndex: newIndex,
165
+ isNavigatingHistory: true,
166
+ },
167
+ false,
168
+ 'goForward',
169
+ );
170
+
171
+ return targetEntry;
172
+ },
173
+
174
+ pushHistory: (entry) => {
175
+ const { historyCurrentIndex, historyEntries } = get();
176
+
177
+ // Create full entry with metadata
178
+ const fullEntry: HistoryEntry = {
179
+ icon: entry.icon,
180
+ metadata: {
181
+ timestamp: Date.now(),
182
+ ...entry.metadata,
183
+ },
184
+ title: entry.title,
185
+ url: entry.url,
186
+ };
187
+
188
+ // If not at the end, truncate forward history
189
+ const newEntries =
190
+ historyCurrentIndex < historyEntries.length - 1
191
+ ? historyEntries.slice(0, historyCurrentIndex + 1)
192
+ : [...historyEntries];
193
+
194
+ // Add new entry
195
+ newEntries.push(fullEntry);
196
+
197
+ set(
198
+ {
199
+ historyCurrentIndex: newEntries.length - 1,
200
+ historyEntries: newEntries,
201
+ },
202
+ false,
203
+ 'pushHistory',
204
+ );
205
+ },
206
+
207
+ replaceHistory: (entry) => {
208
+ const { historyCurrentIndex, historyEntries } = get();
209
+
210
+ // If history is empty, just push
211
+ if (historyCurrentIndex < 0 || historyEntries.length === 0) {
212
+ get().pushHistory(entry);
213
+ return;
214
+ }
215
+
216
+ // Create full entry with metadata
217
+ const fullEntry: HistoryEntry = {
218
+ icon: entry.icon,
219
+ metadata: {
220
+ timestamp: Date.now(),
221
+ ...entry.metadata,
222
+ },
223
+ title: entry.title,
224
+ url: entry.url,
225
+ };
226
+
227
+ // Replace current entry
228
+ const newEntries = [...historyEntries];
229
+ newEntries[historyCurrentIndex] = fullEntry;
230
+
231
+ set(
232
+ {
233
+ historyEntries: newEntries,
234
+ },
235
+ false,
236
+ 'replaceHistory',
237
+ );
238
+ },
239
+
240
+ setCurrentPageTitle: (title) => {
241
+ set({ currentPageTitle: title }, false, 'setCurrentPageTitle');
242
+ },
243
+
244
+ setIsNavigatingHistory: (value) => {
245
+ set({ isNavigatingHistory: value }, false, 'setIsNavigatingHistory');
246
+ },
247
+ });
@@ -4,6 +4,11 @@ import {
4
4
  type NetworkProxySettings,
5
5
  } from '@lobechat/electron-client-ipc';
6
6
 
7
+ import {
8
+ type NavigationHistoryState,
9
+ navigationHistoryInitialState,
10
+ } from './actions/navigationHistory';
11
+
7
12
  export type RemoteServerError = 'CONFIG_ERROR' | 'AUTH_ERROR' | 'DISCONNECT_ERROR';
8
13
 
9
14
  export const defaultProxySettings: NetworkProxySettings = {
@@ -15,7 +20,7 @@ export const defaultProxySettings: NetworkProxySettings = {
15
20
  proxyType: 'http',
16
21
  };
17
22
 
18
- export interface ElectronState {
23
+ export interface ElectronState extends NavigationHistoryState {
19
24
  appState: ElectronAppState;
20
25
  dataSyncConfig: DataSyncConfig;
21
26
  desktopHotkeys: Record<string, string>;
@@ -29,6 +34,7 @@ export interface ElectronState {
29
34
  }
30
35
 
31
36
  export const initialState: ElectronState = {
37
+ ...navigationHistoryInitialState,
32
38
  appState: {},
33
39
  dataSyncConfig: { storageMode: 'cloud' },
34
40
  desktopHotkeys: {},
@@ -4,6 +4,10 @@ import { type StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { createDevtools } from '../middleware/createDevtools';
6
6
  import { type ElectronAppAction, createElectronAppSlice } from './actions/app';
7
+ import {
8
+ type NavigationHistoryAction,
9
+ createNavigationHistorySlice,
10
+ } from './actions/navigationHistory';
7
11
  import { type ElectronSettingsAction, settingsSlice } from './actions/settings';
8
12
  import { type ElectronRemoteServerAction, remoteSyncSlice } from './actions/sync';
9
13
  import { type ElectronState, initialState } from './initialState';
@@ -11,10 +15,12 @@ import { type ElectronState, initialState } from './initialState';
11
15
  // =============== Aggregate createStoreFn ============ //
12
16
 
13
17
  export interface ElectronStore
14
- extends ElectronState,
18
+ extends
19
+ ElectronState,
15
20
  ElectronRemoteServerAction,
16
21
  ElectronAppAction,
17
- ElectronSettingsAction {
22
+ ElectronSettingsAction,
23
+ NavigationHistoryAction {
18
24
  /* empty */
19
25
  }
20
26
 
@@ -25,6 +31,7 @@ const createStore: StateCreator<ElectronStore, [['zustand/devtools', never]]> =
25
31
  ...remoteSyncSlice(...parameters),
26
32
  ...createElectronAppSlice(...parameters),
27
33
  ...settingsSlice(...parameters),
34
+ ...createNavigationHistorySlice(...parameters),
28
35
  });
29
36
 
30
37
  // =============== Implement useStore ============ //
@@ -30,7 +30,10 @@ const modelSwitchPanelWidth = (s: GlobalState) => s.status.modelSwitchPanelWidth
30
30
 
31
31
  const showChatHeader = (s: GlobalState) => !s.status.zenMode;
32
32
  const inZenMode = (s: GlobalState) => s.status.zenMode;
33
- const leftPanelWidth = (s: GlobalState) => s.status.leftPanelWidth;
33
+ const leftPanelWidth = (s: GlobalState): number => {
34
+ const width = s.status.leftPanelWidth;
35
+ return typeof width === 'string' ? Number.parseInt(width) : width;
36
+ };
34
37
  const portalWidth = (s: GlobalState) => s.status.portalWidth || 400;
35
38
  const filePanelWidth = (s: GlobalState) => s.status.filePanelWidth;
36
39
  const imagePanelWidth = (s: GlobalState) => s.status.imagePanelWidth;