diff --git a/apps/desktop/resources/locales/ar/menu.json b/apps/desktop/resources/locales/ar/menu.json index cf56491234..6d0b5b1c2c 100644 --- a/apps/desktop/resources/locales/ar/menu.json +++ b/apps/desktop/resources/locales/ar/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "الإبلاغ عن مشكلة", "help.title": "مساعدة", "help.visitWebsite": "زيارة الموقع الرسمي", + "history.back": "رجوع", + "history.forward": "تقدم", + "history.home": "الرئيسية", + "history.title": "انتقل", "macOS.about": "حول {{appName}}", "macOS.devTools": "أدوات مطور LobeHub", "macOS.hide": "إخفاء {{appName}}", @@ -50,4 +54,4 @@ "window.title": "نافذة", "window.toggleFullscreen": "تبديل وضع ملء الشاشة", "window.zoom": "تكبير" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/bg-BG/menu.json b/apps/desktop/resources/locales/bg-BG/menu.json index da2256cf40..a8a00ce645 100644 --- a/apps/desktop/resources/locales/bg-BG/menu.json +++ b/apps/desktop/resources/locales/bg-BG/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Докладвай проблем", "help.title": "Помощ", "help.visitWebsite": "Посети уебсайта", + "history.back": "Назад", + "history.forward": "Напред", + "history.home": "Начало", + "history.title": "Отиди", "macOS.about": "За {{appName}}", "macOS.devTools": "Инструменти за разработчици на LobeHub", "macOS.hide": "Скрий {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Прозорец", "window.toggleFullscreen": "Превключи на цял екран", "window.zoom": "Мащаб" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/de-DE/menu.json b/apps/desktop/resources/locales/de-DE/menu.json index b102cffff7..d793852dd0 100644 --- a/apps/desktop/resources/locales/de-DE/menu.json +++ b/apps/desktop/resources/locales/de-DE/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Problem melden", "help.title": "Hilfe", "help.visitWebsite": "Besuche die Website", + "history.back": "Zurück", + "history.forward": "Vorwärts", + "history.home": "Start", + "history.title": "Gehen", "macOS.about": "Über {{appName}}", "macOS.devTools": "LobeHub Entwicklerwerkzeuge", "macOS.hide": "{{appName}} ausblenden", @@ -50,4 +54,4 @@ "window.title": "Fenster", "window.toggleFullscreen": "Vollbild umschalten", "window.zoom": "Zoom" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/es-ES/menu.json b/apps/desktop/resources/locales/es-ES/menu.json index d6b1ef1b8b..7cb933b0ef 100644 --- a/apps/desktop/resources/locales/es-ES/menu.json +++ b/apps/desktop/resources/locales/es-ES/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Reportar un problema", "help.title": "Ayuda", "help.visitWebsite": "Visitar el sitio web", + "history.back": "Atrás", + "history.forward": "Adelante", + "history.home": "Inicio", + "history.title": "Ir", "macOS.about": "Acerca de {{appName}}", "macOS.devTools": "Herramientas de desarrollador de LobeHub", "macOS.hide": "Ocultar {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Ventana", "window.toggleFullscreen": "Alternar pantalla completa", "window.zoom": "Zoom" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/fa-IR/menu.json b/apps/desktop/resources/locales/fa-IR/menu.json index 932097f40a..9ef00a86a0 100644 --- a/apps/desktop/resources/locales/fa-IR/menu.json +++ b/apps/desktop/resources/locales/fa-IR/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "گزارش مشکل", "help.title": "کمک", "help.visitWebsite": "بازدید از وب‌سایت", + "history.back": "بازگشت", + "history.forward": "جلو", + "history.home": "خانه", + "history.title": "برو", "macOS.about": "درباره {{appName}}", "macOS.devTools": "ابزارهای توسعه‌دهنده LobeHub", "macOS.hide": "پنهان کردن {{appName}}", @@ -50,4 +54,4 @@ "window.title": "پنجره", "window.toggleFullscreen": "تغییر به حالت تمام صفحه", "window.zoom": "زوم" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/fr-FR/menu.json b/apps/desktop/resources/locales/fr-FR/menu.json index a87977ae87..bdae5f7e12 100644 --- a/apps/desktop/resources/locales/fr-FR/menu.json +++ b/apps/desktop/resources/locales/fr-FR/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Signaler un problème", "help.title": "Aide", "help.visitWebsite": "Visiter le site officiel", + "history.back": "Retour", + "history.forward": "Avancer", + "history.home": "Accueil", + "history.title": "Aller", "macOS.about": "À propos de {{appName}}", "macOS.devTools": "Outils de développement LobeHub", "macOS.hide": "Masquer {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Fenêtre", "window.toggleFullscreen": "Basculer en plein écran", "window.zoom": "Zoom" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/it-IT/menu.json b/apps/desktop/resources/locales/it-IT/menu.json index 42c85c2d45..a46881ee3c 100644 --- a/apps/desktop/resources/locales/it-IT/menu.json +++ b/apps/desktop/resources/locales/it-IT/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Segnala un problema", "help.title": "Aiuto", "help.visitWebsite": "Visita il sito ufficiale", + "history.back": "Indietro", + "history.forward": "Avanti", + "history.home": "Home", + "history.title": "Vai", "macOS.about": "Informazioni su {{appName}}", "macOS.devTools": "Strumenti per sviluppatori LobeHub", "macOS.hide": "Nascondi {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Finestra", "window.toggleFullscreen": "Attiva/disattiva schermo intero", "window.zoom": "Zoom" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/ja-JP/menu.json b/apps/desktop/resources/locales/ja-JP/menu.json index 01b4623d86..958ace25f4 100644 --- a/apps/desktop/resources/locales/ja-JP/menu.json +++ b/apps/desktop/resources/locales/ja-JP/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "問題を報告", "help.title": "ヘルプ", "help.visitWebsite": "公式ウェブサイトを訪問", + "history.back": "戻る", + "history.forward": "進む", + "history.home": "ホーム", + "history.title": "移動", "macOS.about": "{{appName}} について", "macOS.devTools": "LobeHub 開発者ツール", "macOS.hide": "{{appName}} を隠す", @@ -50,4 +54,4 @@ "window.title": "ウィンドウ", "window.toggleFullscreen": "フルスクリーン切替", "window.zoom": "ズーム" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/ko-KR/menu.json b/apps/desktop/resources/locales/ko-KR/menu.json index d54d0ecb75..0430b86ccf 100644 --- a/apps/desktop/resources/locales/ko-KR/menu.json +++ b/apps/desktop/resources/locales/ko-KR/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "문제 보고", "help.title": "도움말", "help.visitWebsite": "웹사이트 방문", + "history.back": "뒤로", + "history.forward": "앞으로", + "history.home": "홈", + "history.title": "이동", "macOS.about": "{{appName}} 정보", "macOS.devTools": "LobeHub 개발자 도구", "macOS.hide": "{{appName}} 숨기기", @@ -50,4 +54,4 @@ "window.title": "창", "window.toggleFullscreen": "전체 화면 전환", "window.zoom": "줌" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/nl-NL/menu.json b/apps/desktop/resources/locales/nl-NL/menu.json index 7f0071e5e3..3e9fdc9614 100644 --- a/apps/desktop/resources/locales/nl-NL/menu.json +++ b/apps/desktop/resources/locales/nl-NL/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Probleem melden", "help.title": "Hulp", "help.visitWebsite": "Bezoek de website", + "history.back": "Terug", + "history.forward": "Vooruit", + "history.home": "Home", + "history.title": "Ga", "macOS.about": "Over {{appName}}", "macOS.devTools": "LobeHub Ontwikkelaarstools", "macOS.hide": "Verberg {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Venster", "window.toggleFullscreen": "Schakel volledig scherm in/uit", "window.zoom": "Inzoomen" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/pl-PL/menu.json b/apps/desktop/resources/locales/pl-PL/menu.json index b0bc1bca1c..7af6ac254c 100644 --- a/apps/desktop/resources/locales/pl-PL/menu.json +++ b/apps/desktop/resources/locales/pl-PL/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Zgłoś problem", "help.title": "Pomoc", "help.visitWebsite": "Odwiedź stronę internetową", + "history.back": "Wstecz", + "history.forward": "Naprzód", + "history.home": "Strona główna", + "history.title": "Idź", "macOS.about": "O {{appName}}", "macOS.devTools": "Narzędzia dewelopera LobeHub", "macOS.hide": "Ukryj {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Okno", "window.toggleFullscreen": "Przełącz tryb pełnoekranowy", "window.zoom": "Powiększenie" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/pt-BR/menu.json b/apps/desktop/resources/locales/pt-BR/menu.json index f65276036c..b560e898ca 100644 --- a/apps/desktop/resources/locales/pt-BR/menu.json +++ b/apps/desktop/resources/locales/pt-BR/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Reportar Problema", "help.title": "Ajuda", "help.visitWebsite": "Visitar o Site", + "history.back": "Voltar", + "history.forward": "Avançar", + "history.home": "Início", + "history.title": "Ir", "macOS.about": "Sobre {{appName}}", "macOS.devTools": "Ferramentas do Desenvolvedor LobeHub", "macOS.hide": "Ocultar {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Janela", "window.toggleFullscreen": "Alternar Tela Cheia", "window.zoom": "Zoom" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/ru-RU/menu.json b/apps/desktop/resources/locales/ru-RU/menu.json index 80fdf5cebc..9764cb28c6 100644 --- a/apps/desktop/resources/locales/ru-RU/menu.json +++ b/apps/desktop/resources/locales/ru-RU/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Сообщить о проблеме", "help.title": "Помощь", "help.visitWebsite": "Посетить сайт", + "history.back": "Назад", + "history.forward": "Вперёд", + "history.home": "Домой", + "history.title": "Перейти", "macOS.about": "О {{appName}}", "macOS.devTools": "Инструменты разработчика LobeHub", "macOS.hide": "Скрыть {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Окно", "window.toggleFullscreen": "Переключить полноэкранный режим", "window.zoom": "Масштаб" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/tr-TR/menu.json b/apps/desktop/resources/locales/tr-TR/menu.json index 68ef042957..242ebf5b2a 100644 --- a/apps/desktop/resources/locales/tr-TR/menu.json +++ b/apps/desktop/resources/locales/tr-TR/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Sorun Bildir", "help.title": "Yardım", "help.visitWebsite": "Resmi Web Sitesini Ziyaret Et", + "history.back": "Geri", + "history.forward": "İleri", + "history.home": "Ana Sayfa", + "history.title": "Git", "macOS.about": "{{appName}} Hakkında", "macOS.devTools": "LobeHub Geliştirici Araçları", "macOS.hide": "{{appName}}'i Gizle", @@ -50,4 +54,4 @@ "window.title": "Pencere", "window.toggleFullscreen": "Tam Ekrana Geç", "window.zoom": "Yakınlaştır" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/vi-VN/menu.json b/apps/desktop/resources/locales/vi-VN/menu.json index 91f84d6535..85bd75935c 100644 --- a/apps/desktop/resources/locales/vi-VN/menu.json +++ b/apps/desktop/resources/locales/vi-VN/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "Báo cáo sự cố", "help.title": "Trợ giúp", "help.visitWebsite": "Truy cập trang web", + "history.back": "Quay lại", + "history.forward": "Tiến tới", + "history.home": "Trang chủ", + "history.title": "Đi", "macOS.about": "Về {{appName}}", "macOS.devTools": "Công cụ phát triển LobeHub", "macOS.hide": "Ẩn {{appName}}", @@ -50,4 +54,4 @@ "window.title": "Cửa sổ", "window.toggleFullscreen": "Chuyển đổi toàn màn hình", "window.zoom": "Thu phóng" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/zh-CN/menu.json b/apps/desktop/resources/locales/zh-CN/menu.json index 4f7f437230..c43a13a99f 100644 --- a/apps/desktop/resources/locales/zh-CN/menu.json +++ b/apps/desktop/resources/locales/zh-CN/menu.json @@ -35,6 +35,10 @@ "help.reportIssue": "反馈问题", "help.title": "帮助", "help.visitWebsite": "打开官网", + "history.back": "后退", + "history.forward": "前进", + "history.home": "主页", + "history.title": "前往", "macOS.about": "关于 {{appName}}", "macOS.devTools": "LobeHub 开发者工具", "macOS.hide": "隐藏 {{appName}}", @@ -59,4 +63,4 @@ "window.title": "窗口", "window.toggleFullscreen": "切换全屏", "window.zoom": "缩放" -} \ No newline at end of file +} diff --git a/apps/desktop/resources/locales/zh-TW/menu.json b/apps/desktop/resources/locales/zh-TW/menu.json index 2d2e8412ae..af924cc806 100644 --- a/apps/desktop/resources/locales/zh-TW/menu.json +++ b/apps/desktop/resources/locales/zh-TW/menu.json @@ -26,6 +26,10 @@ "help.reportIssue": "報告問題", "help.title": "幫助", "help.visitWebsite": "訪問網站", + "history.back": "後退", + "history.forward": "前進", + "history.home": "首頁", + "history.title": "前往", "macOS.about": "關於 {{appName}}", "macOS.devTools": "LobeHub 開發者工具", "macOS.hide": "隱藏 {{appName}}", @@ -50,4 +54,4 @@ "window.title": "視窗", "window.toggleFullscreen": "切換全螢幕", "window.zoom": "縮放" -} \ No newline at end of file +} diff --git a/apps/desktop/src/main/locales/default/menu.ts b/apps/desktop/src/main/locales/default/menu.ts index 3e83852c7d..cf135d8512 100644 --- a/apps/desktop/src/main/locales/default/menu.ts +++ b/apps/desktop/src/main/locales/default/menu.ts @@ -35,6 +35,10 @@ const menu = { 'help.reportIssue': 'Send Feedback', 'help.title': 'Help', 'help.visitWebsite': 'Open Website', + 'history.back': 'Back', + 'history.forward': 'Forward', + 'history.home': 'Home', + 'history.title': 'Go', 'macOS.about': 'About {{appName}}', 'macOS.devTools': 'LobeHub Developer Tools', 'macOS.hide': 'Hide {{appName}}', @@ -61,4 +65,4 @@ const menu = { 'window.zoom': 'Zoom', }; -export default menu; \ No newline at end of file +export default menu; diff --git a/apps/desktop/src/main/menus/impls/linux.ts b/apps/desktop/src/main/menus/impls/linux.ts index 302ec19753..7890b40922 100644 --- a/apps/desktop/src/main/menus/impls/linux.ts +++ b/apps/desktop/src/main/menus/impls/linux.ts @@ -102,6 +102,36 @@ export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform { { accelerator: 'F11', label: t('view.toggleFullscreen'), role: 'togglefullscreen' }, ], }, + { + label: t('history.title'), + submenu: [ + { + accelerator: 'Alt+Left', + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('historyGoBack'); + }, + label: t('history.back'), + }, + { + accelerator: 'Alt+Right', + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('historyGoForward'); + }, + label: t('history.forward'), + }, + { type: 'separator' }, + { + accelerator: 'Ctrl+Shift+H', + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('navigate', { path: '/' }); + }, + label: t('history.home'), + }, + ], + }, { label: t('window.title'), submenu: [ diff --git a/apps/desktop/src/main/menus/impls/macOS.test.ts b/apps/desktop/src/main/menus/impls/macOS.test.ts index b837c19fbd..d3c550e921 100644 --- a/apps/desktop/src/main/menus/impls/macOS.test.ts +++ b/apps/desktop/src/main/menus/impls/macOS.test.ts @@ -292,6 +292,23 @@ describe('MacOSMenu', () => { expect(copyItem.accelerator).toBe('Command+C'); }); + + it('should set correct accelerators for history navigation', () => { + macOSMenu.buildAndSetAppMenu(); + + const template = (Menu.buildFromTemplate as any).mock.calls[0][0]; + const historyMenu = template.find( + (item: any) => item.label === menuTranslations['history.title'], + ); + + const backItem = historyMenu.submenu.find((item: any) => item.label === 'Back'); + const forwardItem = historyMenu.submenu.find((item: any) => item.label === 'Forward'); + const homeItem = historyMenu.submenu.find((item: any) => item.label === 'Home'); + + expect(backItem.accelerator).toBe('Command+['); + expect(forwardItem.accelerator).toBe('Command+]'); + expect(homeItem.accelerator).toBe('Shift+Command+H'); + }); }); describe('developer menu items', () => { diff --git a/apps/desktop/src/main/menus/impls/macOS.ts b/apps/desktop/src/main/menus/impls/macOS.ts index 9f2b0df467..3daeb0cdef 100644 --- a/apps/desktop/src/main/menus/impls/macOS.ts +++ b/apps/desktop/src/main/menus/impls/macOS.ts @@ -166,6 +166,39 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform { { accelerator: 'F11', label: t('view.toggleFullscreen'), role: 'togglefullscreen' }, ], }, + { + label: t('history.title'), + submenu: [ + { + accelerator: 'Command+[', + acceleratorWorksWhenHidden: true, + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('historyGoBack'); + }, + label: t('history.back'), + }, + { + accelerator: 'Command+]', + acceleratorWorksWhenHidden: true, + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('historyGoForward'); + }, + label: t('history.forward'), + }, + { type: 'separator' }, + { + accelerator: 'Shift+Command+H', + acceleratorWorksWhenHidden: true, + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('navigate', { path: '/' }); + }, + label: t('history.home'), + }, + ], + }, { label: t('window.title'), role: 'windowMenu', diff --git a/apps/desktop/src/main/menus/impls/windows.ts b/apps/desktop/src/main/menus/impls/windows.ts index fd0e9a9f8c..1fce1691db 100644 --- a/apps/desktop/src/main/menus/impls/windows.ts +++ b/apps/desktop/src/main/menus/impls/windows.ts @@ -101,6 +101,36 @@ export class WindowsMenu extends BaseMenuPlatform implements IMenuPlatform { { accelerator: 'F11', label: t('view.toggleFullscreen'), role: 'togglefullscreen' }, ], }, + { + label: t('history.title'), + submenu: [ + { + accelerator: 'Alt+Left', + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('historyGoBack'); + }, + label: t('history.back'), + }, + { + accelerator: 'Alt+Right', + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('historyGoForward'); + }, + label: t('history.forward'), + }, + { type: 'separator' }, + { + accelerator: 'Ctrl+Shift+H', + click: () => { + const mainWindow = this.app.browserManager.getMainWindow(); + mainWindow.broadcast('navigate', { path: '/' }); + }, + label: t('history.home'), + }, + ], + }, { label: t('window.title'), submenu: [ diff --git a/locales/ar/electron.json b/locales/ar/electron.json index 395e6f589b..e860f10c3c 100644 --- a/locales/ar/electron.json +++ b/locales/ar/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "محادثة", + "navigation.discover": "اكتشف", + "navigation.discoverAssistants": "اكتشف المساعدين", + "navigation.discoverMcp": "اكتشف MCP", + "navigation.discoverModels": "اكتشف النماذج", + "navigation.discoverProviders": "اكتشف المزودين", + "navigation.group": "مجموعة", + "navigation.groupChat": "محادثة جماعية", + "navigation.home": "الرئيسية", + "navigation.image": "صورة", + "navigation.knowledgeBase": "قاعدة المعرفة", + "navigation.lobehub": "LobeHub", + "navigation.memory": "الذاكرة", + "navigation.memoryContexts": "الذاكرة - السياقات", + "navigation.memoryExperiences": "الذاكرة - التجارب", + "navigation.memoryIdentities": "الذاكرة - الهويات", + "navigation.memoryPreferences": "الذاكرة - التفضيلات", + "navigation.onboarding": "البدء", + "navigation.page": "صفحة", + "navigation.pages": "الصفحات", + "navigation.provider": "المزود", + "navigation.recentView": "المشاهدات الأخيرة", + "navigation.resources": "الموارد", + "navigation.settings": "الإعدادات", "notification.finishChatGeneration": "اكتمل توليد الرسالة بواسطة الذكاء الاصطناعي", "proxy.auth": "يتطلب المصادقة", "proxy.authDesc": "إذا كان خادم البروكسي يتطلب اسم مستخدم وكلمة مرور", diff --git a/locales/bg-BG/electron.json b/locales/bg-BG/electron.json index 37f54df0a3..59fa38b2aa 100644 --- a/locales/bg-BG/electron.json +++ b/locales/bg-BG/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Чат", + "navigation.discover": "Откриване", + "navigation.discoverAssistants": "Откриване на Асистенти", + "navigation.discoverMcp": "Откриване на MCP", + "navigation.discoverModels": "Откриване на Модели", + "navigation.discoverProviders": "Откриване на Доставчици", + "navigation.group": "Група", + "navigation.groupChat": "Групов Чат", + "navigation.home": "Начало", + "navigation.image": "Изображение", + "navigation.knowledgeBase": "База Знания", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Памят", + "navigation.memoryContexts": "Памят - Контексти", + "navigation.memoryExperiences": "Памят - Преживявания", + "navigation.memoryIdentities": "Памят - Идентичности", + "navigation.memoryPreferences": "Памят - Предпочитания", + "navigation.onboarding": "Въведение", + "navigation.page": "Страница", + "navigation.pages": "Страници", + "navigation.provider": "Доставчик", + "navigation.recentView": "Последни преглеждания", + "navigation.resources": "Ресурси", + "navigation.settings": "Настройки", "notification.finishChatGeneration": "Генерирането на съобщение от ИИ е завършено", "proxy.auth": "Изисква се удостоверяване", "proxy.authDesc": "Ако прокси сървърът изисква потребителско име и парола", diff --git a/locales/de-DE/electron.json b/locales/de-DE/electron.json index b80543bb60..a1b41511c5 100644 --- a/locales/de-DE/electron.json +++ b/locales/de-DE/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Entdecken", + "navigation.discoverAssistants": "Assistenten entdecken", + "navigation.discoverMcp": "MCP entdecken", + "navigation.discoverModels": "Modelle entdecken", + "navigation.discoverProviders": "Anbieter entdecken", + "navigation.group": "Gruppe", + "navigation.groupChat": "Gruppen-Chat", + "navigation.home": "Startseite", + "navigation.image": "Bild", + "navigation.knowledgeBase": "Wissensdatenbank", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Speicher", + "navigation.memoryContexts": "Speicher - Kontexte", + "navigation.memoryExperiences": "Speicher - Erfahrungen", + "navigation.memoryIdentities": "Speicher - Identitäten", + "navigation.memoryPreferences": "Speicher - Präferenzen", + "navigation.onboarding": "Einführung", + "navigation.page": "Seite", + "navigation.pages": "Seiten", + "navigation.provider": "Anbieter", + "navigation.recentView": "Zuletzt angesehen", + "navigation.resources": "Ressourcen", + "navigation.settings": "Einstellungen", "notification.finishChatGeneration": "KI-Nachrichtenerstellung abgeschlossen", "proxy.auth": "Authentifizierung erforderlich", "proxy.authDesc": "Falls der Proxy-Server einen Benutzernamen und ein Passwort benötigt", diff --git a/locales/en-US/electron.json b/locales/en-US/electron.json index 74d14671e0..c2080e2808 100644 --- a/locales/en-US/electron.json +++ b/locales/en-US/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Discover", + "navigation.discoverAssistants": "Discover Assistants", + "navigation.discoverMcp": "Discover MCP", + "navigation.discoverModels": "Discover Models", + "navigation.discoverProviders": "Discover Providers", + "navigation.group": "Group", + "navigation.groupChat": "Group Chat", + "navigation.home": "Home", + "navigation.image": "Image", + "navigation.knowledgeBase": "Knowledge Base", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Memory", + "navigation.memoryContexts": "Memory - Contexts", + "navigation.memoryExperiences": "Memory - Experiences", + "navigation.memoryIdentities": "Memory - Identities", + "navigation.memoryPreferences": "Memory - Preferences", + "navigation.onboarding": "Onboarding", + "navigation.page": "Page", + "navigation.pages": "Pages", + "navigation.provider": "Provider", + "navigation.recentView": "Recent pages", + "navigation.resources": "Resources", + "navigation.settings": "Settings", "notification.finishChatGeneration": "AI message generation completed", "proxy.auth": "Authentication Required", "proxy.authDesc": "If the proxy server requires a username and password", diff --git a/locales/es-ES/electron.json b/locales/es-ES/electron.json index 924b973176..80b4f50005 100644 --- a/locales/es-ES/electron.json +++ b/locales/es-ES/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Descubrir", + "navigation.discoverAssistants": "Descubrir Asistentes", + "navigation.discoverMcp": "Descubrir MCP", + "navigation.discoverModels": "Descubrir Modelos", + "navigation.discoverProviders": "Descubrir Proveedores", + "navigation.group": "Grupo", + "navigation.groupChat": "Chat de Grupo", + "navigation.home": "Inicio", + "navigation.image": "Imagen", + "navigation.knowledgeBase": "Base de Conocimiento", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Memoria", + "navigation.memoryContexts": "Memoria - Contextos", + "navigation.memoryExperiences": "Memoria - Experiencias", + "navigation.memoryIdentities": "Memoria - Identidades", + "navigation.memoryPreferences": "Memoria - Preferencias", + "navigation.onboarding": "Incorporación", + "navigation.page": "Página", + "navigation.pages": "Páginas", + "navigation.provider": "Proveedor", + "navigation.recentView": "Vistas recientes", + "navigation.resources": "Recursos", + "navigation.settings": "Configuración", "notification.finishChatGeneration": "Generación de mensaje por IA completada", "proxy.auth": "Autenticación requerida", "proxy.authDesc": "Si el servidor proxy requiere un nombre de usuario y una contraseña", diff --git a/locales/fa-IR/electron.json b/locales/fa-IR/electron.json index a4bd792178..ea50699833 100644 --- a/locales/fa-IR/electron.json +++ b/locales/fa-IR/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "چت", + "navigation.discover": "کشف", + "navigation.discoverAssistants": "کشف دستیاران", + "navigation.discoverMcp": "کشف MCP", + "navigation.discoverModels": "کشف مدل‌ها", + "navigation.discoverProviders": "کشف ارائه‌دهندگان", + "navigation.group": "گروه", + "navigation.groupChat": "چت گروهی", + "navigation.home": "خانه", + "navigation.image": "تصویر", + "navigation.knowledgeBase": "پایگاه دانش", + "navigation.lobehub": "LobeHub", + "navigation.memory": "حافظه", + "navigation.memoryContexts": "حافظه - زمینه‌ها", + "navigation.memoryExperiences": "حافظه - تجربیات", + "navigation.memoryIdentities": "حافظه - هویت‌ها", + "navigation.memoryPreferences": "حافظه - ترجیحات", + "navigation.onboarding": "راه‌اندازی", + "navigation.page": "صفحه", + "navigation.pages": "صفحات", + "navigation.provider": "ارائه‌دهنده", + "navigation.recentView": "مشاهدات اخیر", + "navigation.resources": "منابع", + "navigation.settings": "تنظیمات", "notification.finishChatGeneration": "تولید پیام توسط هوش مصنوعی به پایان رسید", "proxy.auth": "احراز هویت لازم است", "proxy.authDesc": "در صورتی که سرور پروکسی نیاز به نام کاربری و رمز عبور داشته باشد", diff --git a/locales/fr-FR/electron.json b/locales/fr-FR/electron.json index ff269b10f8..8aadd1874f 100644 --- a/locales/fr-FR/electron.json +++ b/locales/fr-FR/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Découvrir", + "navigation.discoverAssistants": "Découvrir les Assistants", + "navigation.discoverMcp": "Découvrir MCP", + "navigation.discoverModels": "Découvrir les Modèles", + "navigation.discoverProviders": "Découvrir les Fournisseurs", + "navigation.group": "Groupe", + "navigation.groupChat": "Chat de Groupe", + "navigation.home": "Accueil", + "navigation.image": "Image", + "navigation.knowledgeBase": "Base de Connaissances", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Mémoire", + "navigation.memoryContexts": "Mémoire - Contextes", + "navigation.memoryExperiences": "Mémoire - Expériences", + "navigation.memoryIdentities": "Mémoire - Identités", + "navigation.memoryPreferences": "Mémoire - Préférences", + "navigation.onboarding": "Intégration", + "navigation.page": "Page", + "navigation.pages": "Pages", + "navigation.provider": "Fournisseur", + "navigation.recentView": "Vues récentes", + "navigation.resources": "Ressources", + "navigation.settings": "Paramètres", "notification.finishChatGeneration": "Génération du message par l'IA terminée", "proxy.auth": "Authentification requise", "proxy.authDesc": "Si le serveur proxy nécessite un nom d'utilisateur et un mot de passe", diff --git a/locales/it-IT/electron.json b/locales/it-IT/electron.json index debf1cdd46..e3810bd79f 100644 --- a/locales/it-IT/electron.json +++ b/locales/it-IT/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Scopri", + "navigation.discoverAssistants": "Scopri Assistenti", + "navigation.discoverMcp": "Scopri MCP", + "navigation.discoverModels": "Scopri Modelli", + "navigation.discoverProviders": "Scopri Provider", + "navigation.group": "Gruppo", + "navigation.groupChat": "Chat di Gruppo", + "navigation.home": "Home", + "navigation.image": "Immagine", + "navigation.knowledgeBase": "Base di Conoscenza", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Memoria", + "navigation.memoryContexts": "Memoria - Contesti", + "navigation.memoryExperiences": "Memoria - Esperienze", + "navigation.memoryIdentities": "Memoria - Identità", + "navigation.memoryPreferences": "Memoria - Preferenze", + "navigation.onboarding": "Onboarding", + "navigation.page": "Pagina", + "navigation.pages": "Pagine", + "navigation.provider": "Provider", + "navigation.recentView": "Visualizzazioni recenti", + "navigation.resources": "Risorse", + "navigation.settings": "Impostazioni", "notification.finishChatGeneration": "Generazione del messaggio AI completata", "proxy.auth": "Autenticazione richiesta", "proxy.authDesc": "Se il server proxy richiede nome utente e password", diff --git a/locales/ja-JP/electron.json b/locales/ja-JP/electron.json index 37d58515ea..1233e1cbaf 100644 --- a/locales/ja-JP/electron.json +++ b/locales/ja-JP/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "チャット", + "navigation.discover": "発見", + "navigation.discoverAssistants": "アシスタントを発見", + "navigation.discoverMcp": "MCP を発見", + "navigation.discoverModels": "モデルを発見", + "navigation.discoverProviders": "プロバイダーを発見", + "navigation.group": "グループ", + "navigation.groupChat": "グループチャット", + "navigation.home": "ホーム", + "navigation.image": "画像", + "navigation.knowledgeBase": "ナレッジベース", + "navigation.lobehub": "LobeHub", + "navigation.memory": "メモリ", + "navigation.memoryContexts": "メモリ - コンテキスト", + "navigation.memoryExperiences": "メモリ - 経験", + "navigation.memoryIdentities": "メモリ - アイデンティティ", + "navigation.memoryPreferences": "メモリ - 設定", + "navigation.onboarding": "オンボーディング", + "navigation.page": "ページ", + "navigation.pages": "ページ", + "navigation.provider": "プロバイダー", + "navigation.recentView": "最近の閲覧", + "navigation.resources": "リソース", + "navigation.settings": "設定", "notification.finishChatGeneration": "AI メッセージの生成が完了しました", "proxy.auth": "認証が必要", "proxy.authDesc": "プロキシサーバーがユーザー名とパスワードを必要とする場合", diff --git a/locales/ko-KR/electron.json b/locales/ko-KR/electron.json index 97897cf4a3..b6bfd5972d 100644 --- a/locales/ko-KR/electron.json +++ b/locales/ko-KR/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "채팅", + "navigation.discover": "발견", + "navigation.discoverAssistants": "어시스턴트 발견", + "navigation.discoverMcp": "MCP 발견", + "navigation.discoverModels": "모델 발견", + "navigation.discoverProviders": "프로바이더 발견", + "navigation.group": "그룹", + "navigation.groupChat": "그룹 채팅", + "navigation.home": "홈", + "navigation.image": "이미지", + "navigation.knowledgeBase": "지식 베이스", + "navigation.lobehub": "LobeHub", + "navigation.memory": "메모리", + "navigation.memoryContexts": "메모리 - 컨텍스트", + "navigation.memoryExperiences": "메모리 - 경험", + "navigation.memoryIdentities": "메모리 - 신원", + "navigation.memoryPreferences": "메모리 - 선호도", + "navigation.onboarding": "온보딩", + "navigation.page": "페이지", + "navigation.pages": "페이지", + "navigation.provider": "프로바이더", + "navigation.recentView": "최근 조회", + "navigation.resources": "리소스", + "navigation.settings": "설정", "notification.finishChatGeneration": "AI 메시지 생성이 완료되었습니다", "proxy.auth": "인증 필요", "proxy.authDesc": "프록시 서버가 사용자 이름과 비밀번호를 요구하는 경우", diff --git a/locales/nl-NL/electron.json b/locales/nl-NL/electron.json index 9e2d954f2f..a7deb90066 100644 --- a/locales/nl-NL/electron.json +++ b/locales/nl-NL/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Ontdekken", + "navigation.discoverAssistants": "Assistenten Ontdekken", + "navigation.discoverMcp": "MCP Ontdekken", + "navigation.discoverModels": "Modellen Ontdekken", + "navigation.discoverProviders": "Providers Ontdekken", + "navigation.group": "Groep", + "navigation.groupChat": "Groepschat", + "navigation.home": "Startpagina", + "navigation.image": "Afbeelding", + "navigation.knowledgeBase": "Kennisbank", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Geheugen", + "navigation.memoryContexts": "Geheugen - Contexten", + "navigation.memoryExperiences": "Geheugen - Ervaringen", + "navigation.memoryIdentities": "Geheugen - Identiteiten", + "navigation.memoryPreferences": "Geheugen - Voorkeuren", + "navigation.onboarding": "Onboarding", + "navigation.page": "Pagina", + "navigation.pages": "Pagina's", + "navigation.provider": "Provider", + "navigation.recentView": "Recente weergaven", + "navigation.resources": "Bronnen", + "navigation.settings": "Instellingen", "notification.finishChatGeneration": "AI-berichtgeneratie voltooid", "proxy.auth": "Authenticatie vereist", "proxy.authDesc": "Indien de proxyserver een gebruikersnaam en wachtwoord vereist", diff --git a/locales/pl-PL/electron.json b/locales/pl-PL/electron.json index fa39434029..ba020fc707 100644 --- a/locales/pl-PL/electron.json +++ b/locales/pl-PL/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Czat", + "navigation.discover": "Odkryj", + "navigation.discoverAssistants": "Odkryj Asystentów", + "navigation.discoverMcp": "Odkryj MCP", + "navigation.discoverModels": "Odkryj Modele", + "navigation.discoverProviders": "Odkryj Dostawców", + "navigation.group": "Grupa", + "navigation.groupChat": "Czat Grupowy", + "navigation.home": "Strona główna", + "navigation.image": "Obraz", + "navigation.knowledgeBase": "Baza Wiedzy", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Pamięć", + "navigation.memoryContexts": "Pamięć - Konteksty", + "navigation.memoryExperiences": "Pamięć - Doświadczenia", + "navigation.memoryIdentities": "Pamięć - Tożsamości", + "navigation.memoryPreferences": "Pamięć - Preferencje", + "navigation.onboarding": "Wprowadzenie", + "navigation.page": "Strona", + "navigation.pages": "Strony", + "navigation.provider": "Dostawca", + "navigation.recentView": "Ostatnie wyświetlenia", + "navigation.resources": "Zasoby", + "navigation.settings": "Ustawienia", "notification.finishChatGeneration": "Generowanie wiadomości AI zakończone", "proxy.auth": "Wymagana autoryzacja", "proxy.authDesc": "Jeśli serwer proxy wymaga nazwy użytkownika i hasła", diff --git a/locales/pt-BR/electron.json b/locales/pt-BR/electron.json index e96f629ff8..096e52e25d 100644 --- a/locales/pt-BR/electron.json +++ b/locales/pt-BR/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Chat", + "navigation.discover": "Descobrir", + "navigation.discoverAssistants": "Descobrir Assistentes", + "navigation.discoverMcp": "Descobrir MCP", + "navigation.discoverModels": "Descobrir Modelos", + "navigation.discoverProviders": "Descobrir Provedores", + "navigation.group": "Grupo", + "navigation.groupChat": "Chat em Grupo", + "navigation.home": "Início", + "navigation.image": "Imagem", + "navigation.knowledgeBase": "Base de Conhecimento", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Memória", + "navigation.memoryContexts": "Memória - Contextos", + "navigation.memoryExperiences": "Memória - Experiências", + "navigation.memoryIdentities": "Memória - Identidades", + "navigation.memoryPreferences": "Memória - Preferências", + "navigation.onboarding": "Integração", + "navigation.page": "Página", + "navigation.pages": "Páginas", + "navigation.provider": "Provedor", + "navigation.recentView": "Visualizações recentes", + "navigation.resources": "Recursos", + "navigation.settings": "Configurações", "notification.finishChatGeneration": "Geração de mensagem pela IA concluída", "proxy.auth": "Autenticação necessária", "proxy.authDesc": "Se o servidor proxy exigir nome de usuário e senha", diff --git a/locales/ru-RU/electron.json b/locales/ru-RU/electron.json index 2695c67564..06990d97e2 100644 --- a/locales/ru-RU/electron.json +++ b/locales/ru-RU/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Чат", + "navigation.discover": "Открыть", + "navigation.discoverAssistants": "Открыть Ассистентов", + "navigation.discoverMcp": "Открыть MCP", + "navigation.discoverModels": "Открыть Модели", + "navigation.discoverProviders": "Открыть Провайдеров", + "navigation.group": "Группа", + "navigation.groupChat": "Групповой Чат", + "navigation.home": "Главная", + "navigation.image": "Изображение", + "navigation.knowledgeBase": "База Знаний", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Память", + "navigation.memoryContexts": "Память - Контексты", + "navigation.memoryExperiences": "Память - Опыт", + "navigation.memoryIdentities": "Память - Идентичности", + "navigation.memoryPreferences": "Память - Предпочтения", + "navigation.onboarding": "Введение", + "navigation.page": "Страница", + "navigation.pages": "Страницы", + "navigation.provider": "Провайдер", + "navigation.recentView": "Недавние просмотры", + "navigation.resources": "Ресурсы", + "navigation.settings": "Настройки", "notification.finishChatGeneration": "Генерация сообщения ИИ завершена", "proxy.auth": "Требуется аутентификация", "proxy.authDesc": "Если прокси-сервер требует имя пользователя и пароль", diff --git a/locales/tr-TR/electron.json b/locales/tr-TR/electron.json index 7d3619b719..c73bc52a00 100644 --- a/locales/tr-TR/electron.json +++ b/locales/tr-TR/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Sohbet", + "navigation.discover": "Keşfet", + "navigation.discoverAssistants": "Asistanları Keşfet", + "navigation.discoverMcp": "MCP'yi Keşfet", + "navigation.discoverModels": "Modelleri Keşfet", + "navigation.discoverProviders": "Sağlayıcıları Keşfet", + "navigation.group": "Grup", + "navigation.groupChat": "Grup Sohbeti", + "navigation.home": "Ana Sayfa", + "navigation.image": "Görsel", + "navigation.knowledgeBase": "Bilgi Bankası", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Bellek", + "navigation.memoryContexts": "Bellek - Bağlamlar", + "navigation.memoryExperiences": "Bellek - Deneyimler", + "navigation.memoryIdentities": "Bellek - Kimlikler", + "navigation.memoryPreferences": "Bellek - Tercihler", + "navigation.onboarding": "Hoş Geldiniz", + "navigation.page": "Sayfa", + "navigation.pages": "Sayfalar", + "navigation.provider": "Sağlayıcı", + "navigation.recentView": "Son görüntülemeler", + "navigation.resources": "Kaynaklar", + "navigation.settings": "Ayarlar", "notification.finishChatGeneration": "Yapay zeka mesaj oluşturma tamamlandı", "proxy.auth": "Kimlik Doğrulama Gerekli", "proxy.authDesc": "Proxy sunucusu kullanıcı adı ve şifre gerektiriyorsa", diff --git a/locales/vi-VN/electron.json b/locales/vi-VN/electron.json index 365c08469e..3bd47b5d11 100644 --- a/locales/vi-VN/electron.json +++ b/locales/vi-VN/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "Trò chuyện", + "navigation.discover": "Khám phá", + "navigation.discoverAssistants": "Khám phá Trợ lý", + "navigation.discoverMcp": "Khám phá MCP", + "navigation.discoverModels": "Khám phá Mô hình", + "navigation.discoverProviders": "Khám phá Nhà cung cấp", + "navigation.group": "Nhóm", + "navigation.groupChat": "Trò chuyện Nhóm", + "navigation.home": "Trang chủ", + "navigation.image": "Hình ảnh", + "navigation.knowledgeBase": "Cơ sở Tri thức", + "navigation.lobehub": "LobeHub", + "navigation.memory": "Bộ nhớ", + "navigation.memoryContexts": "Bộ nhớ - Ngữ cảnh", + "navigation.memoryExperiences": "Bộ nhớ - Trải nghiệm", + "navigation.memoryIdentities": "Bộ nhớ - Danh tính", + "navigation.memoryPreferences": "Bộ nhớ - Tùy chọn", + "navigation.onboarding": "Giới thiệu", + "navigation.page": "Trang", + "navigation.pages": "Trang", + "navigation.provider": "Nhà cung cấp", + "navigation.recentView": "Đã xem gần đây", + "navigation.resources": "Tài nguyên", + "navigation.settings": "Cài đặt", "notification.finishChatGeneration": "Đã hoàn tất tạo tin nhắn AI", "proxy.auth": "Yêu cầu xác thực", "proxy.authDesc": "Nếu máy chủ proxy yêu cầu tên người dùng và mật khẩu", diff --git a/locales/zh-CN/electron.json b/locales/zh-CN/electron.json index 9ec050fc25..70a4b387be 100644 --- a/locales/zh-CN/electron.json +++ b/locales/zh-CN/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "对话", + "navigation.discover": "发现", + "navigation.discoverAssistants": "发现助理", + "navigation.discoverMcp": "发现 MCP", + "navigation.discoverModels": "发现模型", + "navigation.discoverProviders": "发现模型服务商", + "navigation.group": "群组", + "navigation.groupChat": "群组对话", + "navigation.home": "首页", + "navigation.image": "图像", + "navigation.knowledgeBase": "知识库", + "navigation.lobehub": "LobeHub", + "navigation.memory": "记忆", + "navigation.memoryContexts": "记忆 - 上下文", + "navigation.memoryExperiences": "记忆 - 经历", + "navigation.memoryIdentities": "记忆 - 身份", + "navigation.memoryPreferences": "记忆 - 偏好", + "navigation.onboarding": "引导", + "navigation.page": "文稿", + "navigation.pages": "文稿", + "navigation.provider": "模型服务商", + "navigation.recentView": "最近访问", + "navigation.resources": "资源", + "navigation.settings": "设置", "notification.finishChatGeneration": "AI 消息已生成完毕", "proxy.auth": "需要认证", "proxy.authDesc": "如果代理服务器需要用户名和密码", diff --git a/locales/zh-TW/electron.json b/locales/zh-TW/electron.json index 7c420a85a5..7c869af211 100644 --- a/locales/zh-TW/electron.json +++ b/locales/zh-TW/electron.json @@ -1,4 +1,28 @@ { + "navigation.chat": "對話", + "navigation.discover": "發現", + "navigation.discoverAssistants": "發現助理", + "navigation.discoverMcp": "發現 MCP", + "navigation.discoverModels": "發現模型", + "navigation.discoverProviders": "發現模型服務商", + "navigation.group": "群組", + "navigation.groupChat": "群組對話", + "navigation.home": "首頁", + "navigation.image": "圖像", + "navigation.knowledgeBase": "知識庫", + "navigation.lobehub": "LobeHub", + "navigation.memory": "記憶", + "navigation.memoryContexts": "記憶 - 上下文", + "navigation.memoryExperiences": "記憶 - 經歷", + "navigation.memoryIdentities": "記憶 - 身份", + "navigation.memoryPreferences": "記憶 - 偏好", + "navigation.onboarding": "引導", + "navigation.page": "文稿", + "navigation.pages": "文稿", + "navigation.provider": "模型服務商", + "navigation.recentView": "最近訪問", + "navigation.resources": "資源", + "navigation.settings": "設定", "notification.finishChatGeneration": "AI 訊息已生成完畢", "proxy.auth": "需要認證", "proxy.authDesc": "如果代理伺服器需要使用者名稱和密碼", diff --git a/packages/electron-client-ipc/src/events/navigation.ts b/packages/electron-client-ipc/src/events/navigation.ts index 7355dbc794..3afe8ef7f8 100644 --- a/packages/electron-client-ipc/src/events/navigation.ts +++ b/packages/electron-client-ipc/src/events/navigation.ts @@ -1,4 +1,16 @@ export interface NavigationBroadcastEvents { + /** + * Ask renderer to go back in navigation history. + * Triggered from the main process menu. + */ + historyGoBack: () => void; + + /** + * Ask renderer to go forward in navigation history. + * Triggered from the main process menu. + */ + historyGoForward: () => void; + /** * Ask renderer to navigate within the SPA without reloading the whole page. */ diff --git a/src/components/PageTitle/index.tsx b/src/components/PageTitle/index.tsx index 31c2d0520f..850a060b92 100644 --- a/src/components/PageTitle/index.tsx +++ b/src/components/PageTitle/index.tsx @@ -1,10 +1,20 @@ import { BRANDING_NAME } from '@lobechat/business-const'; import { memo, useEffect } from 'react'; +import { isDesktop } from '@/const/version'; +import { useElectronStore } from '@/store/electron'; + const PageTitle = memo<{ title: string }>(({ title }) => { + const setCurrentPageTitle = useElectronStore((s) => s.setCurrentPageTitle); + useEffect(() => { document.title = title ? `${title} · ${BRANDING_NAME}` : BRANDING_NAME; - }, [title]); + + // Sync title to electron store for navigation history + if (isDesktop) { + setCurrentPageTitle(title); + } + }, [title, setCurrentPageTitle]); return null; }); diff --git a/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx b/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx new file mode 100644 index 0000000000..7a015a63b4 --- /dev/null +++ b/src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { Flexbox, Icon } from '@lobehub/ui'; +import { createStaticStyles } from 'antd-style'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { useElectronStore } from '@/store/electron'; +import type { HistoryEntry } from '@/store/electron/actions/navigationHistory'; + +import { getRouteIcon } from '../helpers/routeMetadata'; + +const styles = createStaticStyles(({ css, cssVar }) => ({ + container: css` + overflow-y: auto; + width: 260px; + max-height: 320px; + padding: 4px; + `, + empty: css` + padding-block: 16px; + padding-inline: 12px; + + font-size: 12px; + color: ${cssVar.colorTextTertiary}; + text-align: center; + `, + icon: css` + flex-shrink: 0; + color: ${cssVar.colorTextSecondary}; + `, + item: css` + cursor: pointer; + + overflow: hidden; + + padding-block: 6px; + padding-inline: 8px; + border-radius: ${cssVar.borderRadiusSM}; + + transition: background-color 0.15s ${cssVar.motionEaseInOut}; + + &:hover { + background-color: ${cssVar.colorFillSecondary}; + } + `, + itemActive: css` + background-color: ${cssVar.colorFillTertiary}; + `, + itemTitle: css` + overflow: hidden; + flex: 1; + + font-size: 12px; + color: ${cssVar.colorText}; + text-overflow: ellipsis; + white-space: nowrap; + `, + title: css` + padding-block: 4px; + padding-inline: 8px; + + font-size: 11px; + font-weight: 500; + color: ${cssVar.colorTextTertiary}; + text-transform: uppercase; + letter-spacing: 0.5px; + `, +})); + +interface RecentlyViewedProps { + onClose: () => void; +} + +const RecentlyViewed = memo(({ onClose }) => { + const { t } = useTranslation('electron'); + const navigate = useNavigate(); + const historyEntries = useElectronStore((s) => s.historyEntries); + const historyCurrentIndex = useElectronStore((s) => s.historyCurrentIndex); + const setIsNavigatingHistory = useElectronStore((s) => s.setIsNavigatingHistory); + + const handleClick = (entry: HistoryEntry, index: number) => { + // Set flag to prevent adding duplicate history entry + setIsNavigatingHistory(true); + + // Update the current index in store + useElectronStore.setState({ historyCurrentIndex: index }); + + // Navigate to the selected entry + navigate(entry.url); + + // Close the popover + onClose(); + }; + + // Show entries in reverse order (most recent first), excluding current + const recentEntries = [...historyEntries] + .map((entry, index) => ({ entry, originalIndex: index })) + .reverse(); + + if (recentEntries.length === 0) { + return ( +
+
{t('navigation.recentView')}
+
+ ); + } + + return ( + +
{t('navigation.recentView')}
+ {recentEntries.map(({ entry, originalIndex }) => { + const isActive = originalIndex === historyCurrentIndex; + const RouteIcon = getRouteIcon(entry.url); + + return ( + handleClick(entry, originalIndex)} + > + {RouteIcon && } + {entry.title} + + ); + })} +
+ ); +}); + +RecentlyViewed.displayName = 'RecentlyViewed'; + +export default RecentlyViewed; diff --git a/src/features/ElectronTitlebar/NavigationBar/index.tsx b/src/features/ElectronTitlebar/NavigationBar/index.tsx new file mode 100644 index 0000000000..04fc396101 --- /dev/null +++ b/src/features/ElectronTitlebar/NavigationBar/index.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { ActionIcon, Flexbox, Popover, Tooltip } from '@lobehub/ui'; +import { ArrowLeft, ArrowRight, Clock } from 'lucide-react'; +import { memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; +import { electronStylish } from '@/styles/electron'; +import { isMacOS } from '@/utils/platform'; + +import { useNavigationHistory } from '../hooks/useNavigationHistory'; +import RecentlyViewed from './RecentlyViewed'; + +const isMac = isMacOS(); + +const useNavPanelWidth = () => { + return useGlobalStore(systemStatusSelectors.leftPanelWidth); +}; + +const NavigationBar = memo(() => { + const { t } = useTranslation('electron'); + const { canGoBack, canGoForward, goBack, goForward } = useNavigationHistory(); + const [historyOpen, setHistoryOpen] = useState(false); + // Use ResizeObserver for real-time width updates during resize + const leftPanelWidth = useNavPanelWidth(); + + // Toggle history popover + const toggleHistoryOpen = useCallback(() => { + setHistoryOpen((prev) => !prev); + }, []); + + // Listen for keyboard shortcut ⌘Y / Ctrl+Y + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + const isCmdOrCtrl = isMac ? event.metaKey : event.ctrlKey; + if (isCmdOrCtrl && event.key.toLowerCase() === 'y') { + event.preventDefault(); + toggleHistoryOpen(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [toggleHistoryOpen]); + + // Tooltip content for the clock button + const tooltipContent = t('navigation.recentView'); + + return ( + + + + + setHistoryOpen(false)} />} + onOpenChange={setHistoryOpen} + open={historyOpen} + placement="bottomLeft" + styles={{ content: { padding: 0 } }} + trigger="click" + > +
+ + + +
+
+
+
+ ); +}); + +NavigationBar.displayName = 'NavigationBar'; + +export default NavigationBar; diff --git a/src/features/ElectronTitlebar/helpers/routeMetadata.ts b/src/features/ElectronTitlebar/helpers/routeMetadata.ts new file mode 100644 index 0000000000..838cc7537e --- /dev/null +++ b/src/features/ElectronTitlebar/helpers/routeMetadata.ts @@ -0,0 +1,214 @@ +/** + * Route metadata mapping for navigation history + * Provides title and icon information based on route path + */ +import { + Brain, + Circle, + Compass, + Database, + FileText, + Home, + Image, + type LucideIcon, + MessageSquare, + Rocket, + Settings, + Users, +} from 'lucide-react'; + +export interface RouteMetadata { + icon?: LucideIcon; + /** i18n key for the title (namespace: electron) */ + titleKey: string; + /** Whether this route should use document.title for more specific title */ + useDynamicTitle?: boolean; +} + +interface RoutePattern { + icon?: LucideIcon; + test: (pathname: string) => boolean; + /** i18n key for the title (namespace: electron) */ + titleKey: string; + /** Whether this route should use document.title for more specific title */ + useDynamicTitle?: boolean; +} + +/** + * Route patterns ordered by specificity (most specific first) + */ +const routePatterns: RoutePattern[] = [ + // Settings routes + { + icon: Settings, + test: (p) => p.startsWith('/settings/provider'), + titleKey: 'navigation.provider', + }, + { + icon: Settings, + test: (p) => p.startsWith('/settings'), + titleKey: 'navigation.settings', + }, + + // Agent/Chat routes - use dynamic title for specific chat names + { + icon: MessageSquare, + test: (p) => p.startsWith('/agent/'), + titleKey: 'navigation.chat', + useDynamicTitle: true, + }, + { + icon: MessageSquare, + test: (p) => p === '/agent', + titleKey: 'navigation.chat', + }, + + // Group routes - use dynamic title for specific group names + { + icon: Users, + test: (p) => p.startsWith('/group/'), + titleKey: 'navigation.groupChat', + useDynamicTitle: true, + }, + { + icon: Users, + test: (p) => p === '/group', + titleKey: 'navigation.group', + }, + + // Community/Discover routes + { + icon: Compass, + test: (p) => p.startsWith('/community/assistant'), + titleKey: 'navigation.discoverAssistants', + }, + { + icon: Compass, + test: (p) => p.startsWith('/community/model'), + titleKey: 'navigation.discoverModels', + }, + { + icon: Compass, + test: (p) => p.startsWith('/community/provider'), + titleKey: 'navigation.discoverProviders', + }, + { + icon: Compass, + test: (p) => p.startsWith('/community/mcp'), + titleKey: 'navigation.discoverMcp', + }, + { + icon: Compass, + test: (p) => p.startsWith('/community'), + titleKey: 'navigation.discover', + }, + + // Resource/Knowledge routes + { + icon: Database, + test: (p) => p.startsWith('/resource/library'), + titleKey: 'navigation.knowledgeBase', + }, + { + icon: Database, + test: (p) => p.startsWith('/resource'), + titleKey: 'navigation.resources', + }, + + // Memory routes + { + icon: Brain, + test: (p) => p.startsWith('/memory/identities'), + titleKey: 'navigation.memoryIdentities', + }, + { + icon: Brain, + test: (p) => p.startsWith('/memory/contexts'), + titleKey: 'navigation.memoryContexts', + }, + { + icon: Brain, + test: (p) => p.startsWith('/memory/preferences'), + titleKey: 'navigation.memoryPreferences', + }, + { + icon: Brain, + test: (p) => p.startsWith('/memory/experiences'), + titleKey: 'navigation.memoryExperiences', + }, + { + icon: Brain, + test: (p) => p.startsWith('/memory'), + titleKey: 'navigation.memory', + }, + + // Image routes + { + icon: Image, + test: (p) => p.startsWith('/image'), + titleKey: 'navigation.image', + }, + + // Page routes - use dynamic title for specific page names + { + icon: FileText, + test: (p) => p.startsWith('/page/'), + titleKey: 'navigation.page', + useDynamicTitle: true, + }, + { + icon: FileText, + test: (p) => p === '/page', + titleKey: 'navigation.pages', + }, + + // Onboarding + { + icon: Rocket, + test: (p) => p.startsWith('/desktop-onboarding') || p.startsWith('/onboarding'), + titleKey: 'navigation.onboarding', + }, + + // Home (default) + { + icon: Home, + test: (p) => p === '/' || p === '', + titleKey: 'navigation.home', + }, +]; + +/** + * Get route metadata based on pathname + * @param pathname - The current route pathname + * @returns Route metadata with titleKey, icon, and useDynamicTitle flag + */ +export const getRouteMetadata = (pathname: string): RouteMetadata => { + // Find the first matching pattern + for (const pattern of routePatterns) { + if (pattern.test(pathname)) { + return { + icon: pattern.icon, + titleKey: pattern.titleKey, + useDynamicTitle: pattern.useDynamicTitle, + }; + } + } + + // Default fallback + return { + icon: Circle, + titleKey: 'navigation.lobehub', + }; +}; + +/** + * Get route icon based on pathname or URL + * @param url - The route URL (may include query string) + * @returns LucideIcon component or undefined + */ +export const getRouteIcon = (url: string): LucideIcon | undefined => { + // Extract pathname from URL + const pathname = url.split('?')[0]; + const metadata = getRouteMetadata(pathname); + return metadata.icon; +}; diff --git a/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts b/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts new file mode 100644 index 0000000000..4de6bd39cb --- /dev/null +++ b/src/features/ElectronTitlebar/hooks/useNavigationHistory.ts @@ -0,0 +1,152 @@ +'use client'; + +import { useWatchBroadcast } from '@lobechat/electron-client-ipc'; +import { useCallback, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { useElectronStore } from '@/store/electron'; + +import { getRouteMetadata } from '../helpers/routeMetadata'; + +/** + * Hook to manage navigation history in Electron desktop app + * Provides browser-like back/forward functionality + */ +export const useNavigationHistory = () => { + const { t } = useTranslation('electron'); + const navigate = useNavigate(); + const location = useLocation(); + + // Get store state and actions + const isNavigatingHistory = useElectronStore((s) => s.isNavigatingHistory); + const historyCurrentIndex = useElectronStore((s) => s.historyCurrentIndex); + const historyEntries = useElectronStore((s) => s.historyEntries); + const currentPageTitle = useElectronStore((s) => s.currentPageTitle); + const pushHistory = useElectronStore((s) => s.pushHistory); + const replaceHistory = useElectronStore((s) => s.replaceHistory); + const setIsNavigatingHistory = useElectronStore((s) => s.setIsNavigatingHistory); + const storeGoBack = useElectronStore((s) => s.goBack); + const storeGoForward = useElectronStore((s) => s.goForward); + const canGoBackFn = useElectronStore((s) => s.canGoBack); + const canGoForwardFn = useElectronStore((s) => s.canGoForward); + const getCurrentEntry = useElectronStore((s) => s.getCurrentEntry); + + // Track previous location to avoid duplicate entries + const prevLocationRef = useRef(null); + + // Calculate can go back/forward + const canGoBack = historyCurrentIndex > 0; + const canGoForward = historyCurrentIndex < historyEntries.length - 1; + + /** + * Go back in history + */ + const goBack = useCallback(() => { + if (!canGoBackFn()) return; + + const targetEntry = storeGoBack(); + if (targetEntry) { + navigate(targetEntry.url); + } + }, [canGoBackFn, storeGoBack, navigate]); + + /** + * Go forward in history + */ + const goForward = useCallback(() => { + if (!canGoForwardFn()) return; + + const targetEntry = storeGoForward(); + if (targetEntry) { + navigate(targetEntry.url); + } + }, [canGoForwardFn, storeGoForward, navigate]); + + // Listen to route changes and push history + useEffect(() => { + const currentUrl = location.pathname + location.search; + + // Skip if this is a back/forward navigation + if (isNavigatingHistory) { + setIsNavigatingHistory(false); + prevLocationRef.current = currentUrl; + return; + } + + // Skip if same as previous location + if (prevLocationRef.current === currentUrl) { + return; + } + + // Skip if same as current entry + const currentEntry = getCurrentEntry(); + if (currentEntry?.url === currentUrl) { + prevLocationRef.current = currentUrl; + return; + } + + // Get metadata for this route + const metadata = getRouteMetadata(location.pathname); + const presetTitle = t(metadata.titleKey as any) as string; + + // Push history with preset title (will be updated by PageTitle if useDynamicTitle) + pushHistory({ + metadata: { + timestamp: Date.now(), + }, + title: presetTitle, + url: currentUrl, + }); + + prevLocationRef.current = currentUrl; + }, [ + location.pathname, + location.search, + isNavigatingHistory, + setIsNavigatingHistory, + getCurrentEntry, + pushHistory, + t, + ]); + + // Update current history entry title when PageTitle component updates + useEffect(() => { + if (!currentPageTitle) return; + + const currentEntry = getCurrentEntry(); + if (!currentEntry) return; + + // Check if current route supports dynamic title + const metadata = getRouteMetadata(location.pathname); + if (!metadata.useDynamicTitle) return; + + // Skip if title is already the same + if (currentEntry.title === currentPageTitle) return; + + // Update the current history entry with the dynamic title + replaceHistory({ + ...currentEntry, + title: currentPageTitle, + }); + }, [currentPageTitle, getCurrentEntry, replaceHistory, location.pathname]); + + // Listen to broadcast events from main process (Electron menu) + useWatchBroadcast('historyGoBack', () => { + goBack(); + }); + + useWatchBroadcast('historyGoForward', () => { + goForward(); + }); + + return { + canGoBack, + canGoForward, + currentEntry: getCurrentEntry(), + goBack, + goForward, + historyEntries, + historyIndex: historyCurrentIndex, + }; +}; diff --git a/src/features/ElectronTitlebar/index.tsx b/src/features/ElectronTitlebar/index.tsx index e274e2bb79..7b4ea6fc2d 100644 --- a/src/features/ElectronTitlebar/index.tsx +++ b/src/features/ElectronTitlebar/index.tsx @@ -1,12 +1,13 @@ import { Flexbox } from '@lobehub/ui'; import { Divider } from 'antd'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useElectronStore } from '@/store/electron'; import { electronStylish } from '@/styles/electron'; import { isMacOS } from '@/utils/platform'; import Connection from './Connection'; +import NavigationBar from './NavigationBar'; import { UpdateModal } from './UpdateModal'; import { UpdateNotification } from './UpdateNotification'; import WinControl from './WinControl'; @@ -25,6 +26,15 @@ const TitleBar = memo(() => { useWatchThemeUpdate(); const showWinControl = isAppStateInit && !isMac; + + const padding = useMemo(() => { + if (showWinControl) { + return '0 12px 0 0'; + } + + return '0 12px'; + }, [showWinControl, isMac]); + return ( { height={TITLE_BAR_HEIGHT} horizontal justify={'space-between'} - paddingInline={showWinControl ? '12px 0' : 12} - style={{ minHeight: TITLE_BAR_HEIGHT }} + style={{ minHeight: TITLE_BAR_HEIGHT, padding }} width={'100%'} > -
-
{/* TODO */}
+ diff --git a/src/features/NavHeader/index.tsx b/src/features/NavHeader/index.tsx index 59ddb89179..b99b993022 100644 --- a/src/features/NavHeader/index.tsx +++ b/src/features/NavHeader/index.tsx @@ -2,7 +2,8 @@ import { Flexbox, type FlexboxProps, TooltipGroup } from '@lobehub/ui'; import { type CSSProperties, type ReactNode, memo } from 'react'; import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton'; -import { useNavPanel } from '@/features/NavPanel/hooks/useNavPanel'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; export interface NavHeaderProps extends Omit { children?: ReactNode; @@ -18,7 +19,8 @@ export interface NavHeaderProps extends Omit { const NavHeader = memo( ({ showTogglePanelButton = true, style, children, left, right, styles, ...rest }) => { - const { expand } = useNavPanel(); + const expand = useGlobalStore(systemStatusSelectors.showLeftPanel); + const noContent = !left && !right; if (noContent && expand) return; diff --git a/src/features/NavPanel/components/NavPanelDraggable.tsx b/src/features/NavPanel/components/NavPanelDraggable.tsx new file mode 100644 index 0000000000..1a0391eae1 --- /dev/null +++ b/src/features/NavPanel/components/NavPanelDraggable.tsx @@ -0,0 +1,174 @@ +'use client'; + +import { DraggablePanel } from '@lobehub/ui'; +import { createStaticStyles, cssVar } from 'antd-style'; +import { AnimatePresence, motion } from 'motion/react'; +import { type ReactNode, memo, useMemo, useRef } from 'react'; + +import { USER_DROPDOWN_ICON_ID } from '@/app/[variants]/(main)/home/_layout/Header/components/User'; +import { isDesktop } from '@/const/version'; +import { TOGGLE_BUTTON_ID } from '@/features/NavPanel/ToggleLeftPanelButton'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; +import { isMacOS } from '@/utils/platform'; + +import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel'; +import { BACK_BUTTON_ID } from './BackButton'; + +const motionVariants = { + animate: { opacity: 1, x: 0 }, + exit: { + opacity: 0, + x: '-20%', + }, + initial: { + opacity: 0, + x: 0, + }, + transition: { + duration: 0.4, + ease: [0.4, 0, 0.2, 1], + }, +} as const; + +const draggableStyles = createStaticStyles(({ css, cssVar }) => ({ + content: css` + position: relative; + + overflow: hidden; + display: flex; + + height: 100%; + min-height: 100%; + max-height: 100%; + `, + inner: css` + position: relative; + inset: 0; + + overflow: hidden; + flex: 1; + flex-direction: column; + + min-width: 240px; + `, + panel: css` + user-select: none; + height: 100%; + color: ${cssVar.colorTextSecondary}; + background: ${isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout}; + + * { + user-select: none; + } + + #${TOGGLE_BUTTON_ID} { + width: 0 !important; + opacity: 0; + transition: + opacity, + width 0.2s ${cssVar.motionEaseOut}; + } + + #${USER_DROPDOWN_ICON_ID} { + width: 0 !important; + opacity: 0; + transition: + opacity, + width 0.2s ${cssVar.motionEaseOut}; + } + + #${BACK_BUTTON_ID} { + width: 0 !important; + opacity: 0; + transition: all 0.2s ${cssVar.motionEaseOut}; + } + + &:hover { + #${TOGGLE_BUTTON_ID} { + width: 32px !important; + opacity: 1; + } + + #${USER_DROPDOWN_ICON_ID} { + width: 14px !important; + opacity: 1; + } + + &:hover { + #${BACK_BUTTON_ID} { + width: 24px !important; + opacity: 1; + } + } + } + `, +})); + +interface NavPanelDraggableProps { + activeContent: { + key: string; + node: ReactNode; + }; +} + +const classNames = { + content: draggableStyles.content, +}; + +export const NavPanelDraggable = memo(({ activeContent }) => { + const [expand, togglePanel] = useGlobalStore((s) => [ + systemStatusSelectors.showLeftPanel(s), + s.toggleLeftPanel, + ]); + const handleSizeChange = useNavPanelSizeChangeHandler(); + + const defaultWidthRef = useRef(0); + if (defaultWidthRef.current === 0) { + defaultWidthRef.current = systemStatusSelectors.leftPanelWidth(useGlobalStore.getState()); + } + + const defaultSize = useMemo( + () => ({ + height: '100%', + width: defaultWidthRef.current, + }), + [defaultWidthRef.current], + ); + const styles = useMemo( + () => ({ + background: isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout, + zIndex: 11, + }), + [isDesktop, isMacOS()], + ); + return ( + + + + {activeContent.node} + + + + ); +}); diff --git a/src/features/NavPanel/hooks/useNavPanel.ts b/src/features/NavPanel/hooks/useNavPanel.ts index cf0d567594..ee87050679 100644 --- a/src/features/NavPanel/hooks/useNavPanel.ts +++ b/src/features/NavPanel/hooks/useNavPanel.ts @@ -2,49 +2,25 @@ import { type DraggablePanelProps } from '@lobehub/ui'; import isEqual from 'fast-deep-equal'; -import { useCallback, useState } from 'react'; +import { useTypeScriptHappyCallback } from '@/hooks/useTypeScriptHappyCallback'; import { useGlobalStore } from '@/store/global'; import { systemStatusSelectors } from '@/store/global/selectors'; -export const useNavPanel = () => { - const [leftPanelWidth, sessionExpandable, togglePanel, updatePreference] = useGlobalStore((s) => [ - systemStatusSelectors.leftPanelWidth(s), - systemStatusSelectors.showLeftPanel(s), - s.toggleLeftPanel, - s.updateSystemStatus, - ]); - - const [tmpWidth, setWidth] = useState(leftPanelWidth); - - if (tmpWidth !== leftPanelWidth) setWidth(leftPanelWidth); - - const handleSizeChange: DraggablePanelProps['onSizeChange'] = useCallback( - (_: any, size: any) => { - const width = size?.width; +export const useNavPanelSizeChangeHandler = (onChange?: (width: number) => void) => { + const handleSizeChange: DraggablePanelProps['onSizeChange'] = useTypeScriptHappyCallback( + (_, size) => { + const width = typeof size?.width === 'string' ? Number.parseInt(size.width) : size?.width; if (!width || width < 64) return; + const s = useGlobalStore.getState(); + const leftPanelWidth = systemStatusSelectors.leftPanelWidth(s); + const updatePreference = s.updateSystemStatus; if (isEqual(width, leftPanelWidth)) return; - setWidth(width); + onChange?.(width); updatePreference({ leftPanelWidth: width }); }, - [sessionExpandable, leftPanelWidth, updatePreference], + [], ); - const openPanel = useCallback(() => { - togglePanel(true); - }, [togglePanel]); - - const closePanel = useCallback(() => { - togglePanel(false); - }, [togglePanel]); - - return { - closePanel, - defaultWidth: tmpWidth, - expand: sessionExpandable, - handleSizeChange, - openPanel, - togglePanel, - width: leftPanelWidth, - }; + return handleSizeChange; }; diff --git a/src/features/NavPanel/index.tsx b/src/features/NavPanel/index.tsx index ce6d399759..760f757a6e 100644 --- a/src/features/NavPanel/index.tsx +++ b/src/features/NavPanel/index.tsx @@ -1,8 +1,5 @@ 'use client'; -import { DraggablePanel } from '@lobehub/ui'; -import { createStaticStyles, cssVar } from 'antd-style'; -import { AnimatePresence, motion } from 'motion/react'; import { type PropsWithChildren, type ReactNode, @@ -11,14 +8,8 @@ import { useSyncExternalStore, } from 'react'; -import { USER_DROPDOWN_ICON_ID } from '@/app/[variants]/(main)/home/_layout/Header/components/User'; -import { isDesktop } from '@/const/version'; -import { TOGGLE_BUTTON_ID } from '@/features/NavPanel/ToggleLeftPanelButton'; -import { isMacOS } from '@/utils/platform'; - import Sidebar from '../../app/[variants]/(main)/home/_layout/Sidebar'; -import { BACK_BUTTON_ID } from './components/BackButton'; -import { useNavPanel } from './hooks/useNavPanel'; +import { NavPanelDraggable } from './components/NavPanelDraggable'; export const NAV_PANEL_RIGHT_DRAWER_ID = 'nav-panel-drawer'; @@ -41,82 +32,7 @@ const setNavPanelSnapshot = (snapshot: NavPanelSnapshot) => { listeners.forEach((listener) => listener()); }; -export const styles = createStaticStyles(({ css, cssVar }) => ({ - content: css` - position: relative; - - overflow: hidden; - display: flex; - - height: 100%; - min-height: 100%; - max-height: 100%; - `, - inner: css` - position: relative; - inset: 0; - - overflow: hidden; - flex: 1; - flex-direction: column; - - min-width: 240px; - `, - panel: css` - user-select: none; - height: 100%; - color: ${cssVar.colorTextSecondary}; - background: ${isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout}; - - * { - user-select: none; - } - - #${TOGGLE_BUTTON_ID} { - width: 0 !important; - opacity: 0; - transition: - opacity, - width 0.2s ${cssVar.motionEaseOut}; - } - - #${USER_DROPDOWN_ICON_ID} { - width: 0 !important; - opacity: 0; - transition: - opacity, - width 0.2s ${cssVar.motionEaseOut}; - } - - #${BACK_BUTTON_ID} { - width: 0 !important; - opacity: 0; - transition: all 0.2s ${cssVar.motionEaseOut}; - } - - &:hover { - #${TOGGLE_BUTTON_ID} { - width: 32px !important; - opacity: 1; - } - - #${USER_DROPDOWN_ICON_ID} { - width: 14px !important; - opacity: 1; - } - - &:hover { - #${BACK_BUTTON_ID} { - width: 24px !important; - opacity: 1; - } - } - } - `, -})); - const NavPanel = memo(() => { - const { expand, handleSizeChange, width, togglePanel } = useNavPanel(); const panelContent = useSyncExternalStore( subscribeNavPanel, getNavPanelSnapshot, @@ -128,47 +44,7 @@ const NavPanel = memo(() => { return ( <> - togglePanel(expand)} - onSizeChange={handleSizeChange} - placement="left" - showBorder={false} - style={{ - background: isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout, - zIndex: 11, - }} - > - - - {activeContent.node} - - - +
( + fn: (...args: Args) => R, + deps: DependencyList, +) => (...args: Args) => R = useCallback; diff --git a/src/locales/default/electron.ts b/src/locales/default/electron.ts index 7183fe7cc1..a6f7d6b738 100644 --- a/src/locales/default/electron.ts +++ b/src/locales/default/electron.ts @@ -1,4 +1,28 @@ export default { + 'navigation.chat': 'Chat', + 'navigation.discover': 'Discover', + 'navigation.discoverAssistants': 'Discover Assistants', + 'navigation.discoverMcp': 'Discover MCP', + 'navigation.discoverModels': 'Discover Models', + 'navigation.discoverProviders': 'Discover Providers', + 'navigation.group': 'Group', + 'navigation.groupChat': 'Group Chat', + 'navigation.home': 'Home', + 'navigation.image': 'Image', + 'navigation.knowledgeBase': 'Knowledge Base', + 'navigation.lobehub': 'LobeHub', + 'navigation.memory': 'Memory', + 'navigation.memoryContexts': 'Memory - Contexts', + 'navigation.memoryExperiences': 'Memory - Experiences', + 'navigation.memoryIdentities': 'Memory - Identities', + 'navigation.memoryPreferences': 'Memory - Preferences', + 'navigation.onboarding': 'Onboarding', + 'navigation.page': 'Page', + 'navigation.pages': 'Pages', + 'navigation.provider': 'Provider', + 'navigation.recentView': 'Recent pages', + 'navigation.resources': 'Resources', + 'navigation.settings': 'Settings', 'notification.finishChatGeneration': 'AI message generation completed', 'proxy.auth': 'Authentication Required', 'proxy.authDesc': 'If the proxy server requires a username and password', diff --git a/src/store/electron/actions/navigationHistory.ts b/src/store/electron/actions/navigationHistory.ts new file mode 100644 index 0000000000..45f61db78a --- /dev/null +++ b/src/store/electron/actions/navigationHistory.ts @@ -0,0 +1,247 @@ +import { type StateCreator } from 'zustand/vanilla'; + +import type { ElectronStore } from '../store'; + +// ======== Types ======== // + +export interface HistoryEntry { + icon?: string; + metadata?: { + [key: string]: any; + sessionId?: string; + timestamp: number; + }; + title: string; + url: string; +} + +export interface NavigationHistoryState { + /** + * Current page title from PageTitle component + * Used to get dynamic titles without setTimeout hack + */ + currentPageTitle: string; + /** + * Current position in history (-1 means empty) + */ + historyCurrentIndex: number; + /** + * History entries list + */ + historyEntries: HistoryEntry[]; + /** + * Flag to indicate if currently navigating via back/forward + * Used to prevent adding duplicate history entries + */ + isNavigatingHistory: boolean; +} + +// ======== Action Interface ======== // + +export interface NavigationHistoryAction { + /** + * Check if can go back in history + */ + canGoBack: () => boolean; + + /** + * Check if can go forward in history + */ + canGoForward: () => boolean; + + /** + * Get current history entry + */ + getCurrentEntry: () => HistoryEntry | null; + + /** + * Navigate back in history + * @returns The target entry or null if cannot go back + */ + goBack: () => HistoryEntry | null; + + /** + * Navigate forward in history + * @returns The target entry or null if cannot go forward + */ + goForward: () => HistoryEntry | null; + + /** + * Push a new entry to history (for normal navigation) + * Truncates any forward history if not at the end + */ + pushHistory: ( + entry: Omit & { metadata?: Partial }, + ) => void; + + /** + * Replace current entry in history (for replace navigation) + */ + replaceHistory: ( + entry: Omit & { metadata?: Partial }, + ) => void; + + /** + * Set current page title (called by PageTitle component) + */ + setCurrentPageTitle: (title: string) => void; + + /** + * Set the navigating history flag + */ + setIsNavigatingHistory: (value: boolean) => void; +} + +// ======== Initial State ======== // + +export const navigationHistoryInitialState: NavigationHistoryState = { + currentPageTitle: '', + historyCurrentIndex: -1, + historyEntries: [], + isNavigatingHistory: false, +}; + +// ======== Action Implementation ======== // + +export const createNavigationHistorySlice: StateCreator< + ElectronStore, + [['zustand/devtools', never]], + [], + NavigationHistoryAction +> = (set, get) => ({ + canGoBack: () => { + const { historyCurrentIndex } = get(); + return historyCurrentIndex > 0; + }, + + canGoForward: () => { + const { historyCurrentIndex, historyEntries } = get(); + return historyCurrentIndex < historyEntries.length - 1; + }, + + getCurrentEntry: () => { + const { historyCurrentIndex, historyEntries } = get(); + if (historyCurrentIndex < 0 || historyCurrentIndex >= historyEntries.length) { + return null; + } + return historyEntries[historyCurrentIndex]; + }, + + goBack: () => { + const { historyCurrentIndex, historyEntries } = get(); + + if (historyCurrentIndex <= 0) { + return null; + } + + const newIndex = historyCurrentIndex - 1; + const targetEntry = historyEntries[newIndex]; + + set( + { + historyCurrentIndex: newIndex, + isNavigatingHistory: true, + }, + false, + 'goBack', + ); + + return targetEntry; + }, + + goForward: () => { + const { historyCurrentIndex, historyEntries } = get(); + + if (historyCurrentIndex >= historyEntries.length - 1) { + return null; + } + + const newIndex = historyCurrentIndex + 1; + const targetEntry = historyEntries[newIndex]; + + set( + { + historyCurrentIndex: newIndex, + isNavigatingHistory: true, + }, + false, + 'goForward', + ); + + return targetEntry; + }, + + pushHistory: (entry) => { + const { historyCurrentIndex, historyEntries } = get(); + + // Create full entry with metadata + const fullEntry: HistoryEntry = { + icon: entry.icon, + metadata: { + timestamp: Date.now(), + ...entry.metadata, + }, + title: entry.title, + url: entry.url, + }; + + // If not at the end, truncate forward history + const newEntries = + historyCurrentIndex < historyEntries.length - 1 + ? historyEntries.slice(0, historyCurrentIndex + 1) + : [...historyEntries]; + + // Add new entry + newEntries.push(fullEntry); + + set( + { + historyCurrentIndex: newEntries.length - 1, + historyEntries: newEntries, + }, + false, + 'pushHistory', + ); + }, + + replaceHistory: (entry) => { + const { historyCurrentIndex, historyEntries } = get(); + + // If history is empty, just push + if (historyCurrentIndex < 0 || historyEntries.length === 0) { + get().pushHistory(entry); + return; + } + + // Create full entry with metadata + const fullEntry: HistoryEntry = { + icon: entry.icon, + metadata: { + timestamp: Date.now(), + ...entry.metadata, + }, + title: entry.title, + url: entry.url, + }; + + // Replace current entry + const newEntries = [...historyEntries]; + newEntries[historyCurrentIndex] = fullEntry; + + set( + { + historyEntries: newEntries, + }, + false, + 'replaceHistory', + ); + }, + + setCurrentPageTitle: (title) => { + set({ currentPageTitle: title }, false, 'setCurrentPageTitle'); + }, + + setIsNavigatingHistory: (value) => { + set({ isNavigatingHistory: value }, false, 'setIsNavigatingHistory'); + }, +}); diff --git a/src/store/electron/initialState.ts b/src/store/electron/initialState.ts index 86ae6ac25b..4815a9f23b 100644 --- a/src/store/electron/initialState.ts +++ b/src/store/electron/initialState.ts @@ -4,6 +4,11 @@ import { type NetworkProxySettings, } from '@lobechat/electron-client-ipc'; +import { + type NavigationHistoryState, + navigationHistoryInitialState, +} from './actions/navigationHistory'; + export type RemoteServerError = 'CONFIG_ERROR' | 'AUTH_ERROR' | 'DISCONNECT_ERROR'; export const defaultProxySettings: NetworkProxySettings = { @@ -15,7 +20,7 @@ export const defaultProxySettings: NetworkProxySettings = { proxyType: 'http', }; -export interface ElectronState { +export interface ElectronState extends NavigationHistoryState { appState: ElectronAppState; dataSyncConfig: DataSyncConfig; desktopHotkeys: Record; @@ -29,6 +34,7 @@ export interface ElectronState { } export const initialState: ElectronState = { + ...navigationHistoryInitialState, appState: {}, dataSyncConfig: { storageMode: 'cloud' }, desktopHotkeys: {}, diff --git a/src/store/electron/store.ts b/src/store/electron/store.ts index ca31c0949c..cac475740c 100644 --- a/src/store/electron/store.ts +++ b/src/store/electron/store.ts @@ -4,6 +4,10 @@ import { type StateCreator } from 'zustand/vanilla'; import { createDevtools } from '../middleware/createDevtools'; import { type ElectronAppAction, createElectronAppSlice } from './actions/app'; +import { + type NavigationHistoryAction, + createNavigationHistorySlice, +} from './actions/navigationHistory'; import { type ElectronSettingsAction, settingsSlice } from './actions/settings'; import { type ElectronRemoteServerAction, remoteSyncSlice } from './actions/sync'; import { type ElectronState, initialState } from './initialState'; @@ -11,10 +15,12 @@ import { type ElectronState, initialState } from './initialState'; // =============== Aggregate createStoreFn ============ // export interface ElectronStore - extends ElectronState, + extends + ElectronState, ElectronRemoteServerAction, ElectronAppAction, - ElectronSettingsAction { + ElectronSettingsAction, + NavigationHistoryAction { /* empty */ } @@ -25,6 +31,7 @@ const createStore: StateCreator = ...remoteSyncSlice(...parameters), ...createElectronAppSlice(...parameters), ...settingsSlice(...parameters), + ...createNavigationHistorySlice(...parameters), }); // =============== Implement useStore ============ // diff --git a/src/store/global/selectors/systemStatus.ts b/src/store/global/selectors/systemStatus.ts index dd997a6a0c..1c8a8d2bdb 100644 --- a/src/store/global/selectors/systemStatus.ts +++ b/src/store/global/selectors/systemStatus.ts @@ -30,7 +30,10 @@ const modelSwitchPanelWidth = (s: GlobalState) => s.status.modelSwitchPanelWidth const showChatHeader = (s: GlobalState) => !s.status.zenMode; const inZenMode = (s: GlobalState) => s.status.zenMode; -const leftPanelWidth = (s: GlobalState) => s.status.leftPanelWidth; +const leftPanelWidth = (s: GlobalState): number => { + const width = s.status.leftPanelWidth; + return typeof width === 'string' ? Number.parseInt(width) : width; +}; const portalWidth = (s: GlobalState) => s.status.portalWidth || 400; const filePanelWidth = (s: GlobalState) => s.status.filePanelWidth; const imagePanelWidth = (s: GlobalState) => s.status.imagePanelWidth;