mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat(desktop): implement history navigation stack (#11341)
* ✨ feat(navigation): implement history navigation in the desktop app - Add 'Back' and 'Forward' options to the menu for navigating history. - Introduce a new NavigationBar component to handle navigation actions. - Implement hooks for managing navigation history and updating the UI accordingly. - Enhance the Electron store to support navigation history state management. - Add route metadata for better navigation context. This update improves user experience by allowing easy back and forward navigation within the app. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat(localization): add navigation labels in multiple languages - Introduced new localization entries for navigation history in various languages, including Arabic, Bulgarian, German, Spanish, Persian, French, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Turkish, Vietnamese, Chinese (Simplified and Traditional). - Updated existing localization files to include 'Back', 'Forward', and 'Go' labels for improved user navigation experience. This enhancement supports a more inclusive user interface by providing localized navigation options. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat(desktop): add Home menu item and simplify navigation UI - Remove keyboard shortcut hints from Recently Viewed tooltip - Add Home menu item to Go menu on all platforms (macOS, Linux, Windows) - Add Home translations for all 17 supported locales * 🌐 i18n(desktop): use i18n for Recently Viewed tooltip * ✨ feat(macOS): update history navigation accelerators in menu - Change keyboard shortcuts for 'Back', 'Forward', and 'Home' menu items to use macOS conventions. - Add unit test to verify correct accelerators are set for history navigation. Signed-off-by: Innei <tukon479@gmail.com> * ✨ refactor(ElectronTitlebar): remove unused navigation history hook - Deleted the `useInitNavigationHistory` hook and its associated logic from the ElectronTitlebar component. - Cleaned up the code to improve maintainability and reduce unnecessary complexity. This change streamlines the title bar functionality by eliminating unused code. Signed-off-by: Innei <tukon479@gmail.com> * ✨ refactor(NavPanel): streamline navigation panel functionality - Replaced the `useNavPanel` hook with a new `useNavPanelSizeChangeHandler` for better size management. - Introduced `NavPanelDraggable` component to encapsulate draggable panel logic, improving code organization and readability. - Updated `NavHeader` to utilize global store for panel state management, enhancing state consistency across components. - Removed unused styles and logic from `NavPanel`, simplifying the component structure. This refactor enhances maintainability and performance of the navigation panel system. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -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": "تكبير"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Мащаб"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "زوم"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "ズーム"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "줌"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Масштаб"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "缩放"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "縮放"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
export default menu;
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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": "إذا كان خادم البروكسي يتطلب اسم مستخدم وكلمة مرور",
|
||||
|
||||
@@ -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": "Ако прокси сървърът изисква потребителско име и парола",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "در صورتی که سرور پروکسی نیاز به نام کاربری و رمز عبور داشته باشد",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "プロキシサーバーがユーザー名とパスワードを必要とする場合",
|
||||
|
||||
@@ -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": "프록시 서버가 사용자 이름과 비밀번호를 요구하는 경우",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Если прокси-сервер требует имя пользователя и пароль",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "如果代理服务器需要用户名和密码",
|
||||
|
||||
@@ -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": "如果代理伺服器需要使用者名稱和密碼",
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
137
src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx
Normal file
137
src/features/ElectronTitlebar/NavigationBar/RecentlyViewed.tsx
Normal file
@@ -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<RecentlyViewedProps>(({ 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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.empty}>{t('navigation.recentView')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container}>
|
||||
<div className={styles.title}>{t('navigation.recentView')}</div>
|
||||
{recentEntries.map(({ entry, originalIndex }) => {
|
||||
const isActive = originalIndex === historyCurrentIndex;
|
||||
const RouteIcon = getRouteIcon(entry.url);
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
className={`${styles.item} ${isActive ? styles.itemActive : ''}`}
|
||||
gap={8}
|
||||
horizontal
|
||||
key={`${entry.url}-${originalIndex}`}
|
||||
onClick={() => handleClick(entry, originalIndex)}
|
||||
>
|
||||
{RouteIcon && <Icon className={styles.icon} icon={RouteIcon} size="small" />}
|
||||
<span className={styles.itemTitle}>{entry.title}</span>
|
||||
</Flexbox>
|
||||
);
|
||||
})}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
RecentlyViewed.displayName = 'RecentlyViewed';
|
||||
|
||||
export default RecentlyViewed;
|
||||
86
src/features/ElectronTitlebar/NavigationBar/index.tsx
Normal file
86
src/features/ElectronTitlebar/NavigationBar/index.tsx
Normal file
@@ -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 (
|
||||
<Flexbox
|
||||
align="center"
|
||||
className={electronStylish.nodrag}
|
||||
data-width={leftPanelWidth}
|
||||
horizontal
|
||||
justify="end"
|
||||
style={{ width: `${leftPanelWidth - 12}px` }}
|
||||
>
|
||||
<Flexbox align="center" gap={2} horizontal>
|
||||
<ActionIcon disabled={!canGoBack} icon={ArrowLeft} onClick={goBack} size="small" />
|
||||
<ActionIcon disabled={!canGoForward} icon={ArrowRight} onClick={goForward} size="small" />
|
||||
<Popover
|
||||
content={<RecentlyViewed onClose={() => setHistoryOpen(false)} />}
|
||||
onOpenChange={setHistoryOpen}
|
||||
open={historyOpen}
|
||||
placement="bottomLeft"
|
||||
styles={{ content: { padding: 0 } }}
|
||||
trigger="click"
|
||||
>
|
||||
<div>
|
||||
<Tooltip open={historyOpen ? false : undefined} title={tooltipContent}>
|
||||
<ActionIcon icon={Clock} size="small" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Popover>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
NavigationBar.displayName = 'NavigationBar';
|
||||
|
||||
export default NavigationBar;
|
||||
214
src/features/ElectronTitlebar/helpers/routeMetadata.ts
Normal file
214
src/features/ElectronTitlebar/helpers/routeMetadata.ts
Normal file
@@ -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;
|
||||
};
|
||||
152
src/features/ElectronTitlebar/hooks/useNavigationHistory.ts
Normal file
152
src/features/ElectronTitlebar/hooks/useNavigationHistory.ts
Normal file
@@ -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<string | null>(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,
|
||||
};
|
||||
};
|
||||
@@ -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 (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
@@ -32,12 +42,10 @@ const TitleBar = memo(() => {
|
||||
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%'}
|
||||
>
|
||||
<div />
|
||||
<div>{/* TODO */}</div>
|
||||
<NavigationBar />
|
||||
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Flexbox className={electronStylish.nodrag} gap={8} horizontal>
|
||||
|
||||
@@ -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<FlexboxProps, 'children'> {
|
||||
children?: ReactNode;
|
||||
@@ -18,7 +19,8 @@ export interface NavHeaderProps extends Omit<FlexboxProps, 'children'> {
|
||||
|
||||
const NavHeader = memo<NavHeaderProps>(
|
||||
({ showTogglePanelButton = true, style, children, left, right, styles, ...rest }) => {
|
||||
const { expand } = useNavPanel();
|
||||
const expand = useGlobalStore(systemStatusSelectors.showLeftPanel);
|
||||
|
||||
const noContent = !left && !right;
|
||||
|
||||
if (noContent && expand) return;
|
||||
|
||||
174
src/features/NavPanel/components/NavPanelDraggable.tsx
Normal file
174
src/features/NavPanel/components/NavPanelDraggable.tsx
Normal file
@@ -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<NavPanelDraggableProps>(({ 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 (
|
||||
<DraggablePanel
|
||||
className={draggableStyles.panel}
|
||||
classNames={classNames}
|
||||
defaultSize={defaultSize}
|
||||
expand={expand}
|
||||
expandable={false}
|
||||
maxWidth={400}
|
||||
minWidth={240}
|
||||
onExpandChange={togglePanel}
|
||||
onSizeDragging={handleSizeChange}
|
||||
placement="left"
|
||||
showBorder={false}
|
||||
style={styles}
|
||||
>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate={motionVariants.animate}
|
||||
className={draggableStyles.inner}
|
||||
exit={motionVariants.exit}
|
||||
initial={motionVariants.initial}
|
||||
key={activeContent.key}
|
||||
transition={motionVariants.transition}
|
||||
>
|
||||
{activeContent.node}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</DraggablePanel>
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<DraggablePanel
|
||||
className={styles.panel}
|
||||
classNames={{
|
||||
content: styles.content,
|
||||
}}
|
||||
defaultSize={{ height: '100%', width }}
|
||||
expand={expand}
|
||||
expandable={false}
|
||||
maxWidth={400}
|
||||
minWidth={240}
|
||||
onExpandChange={(expand) => togglePanel(expand)}
|
||||
onSizeChange={handleSizeChange}
|
||||
placement="left"
|
||||
showBorder={false}
|
||||
style={{
|
||||
background: isDesktop && isMacOS() ? 'transparent' : cssVar.colorBgLayout,
|
||||
zIndex: 11,
|
||||
}}
|
||||
>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={styles.inner}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: '-20%',
|
||||
}}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
x: 0,
|
||||
}}
|
||||
key={activeContent.key}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.4, 0, 0.2, 1],
|
||||
}}
|
||||
>
|
||||
{activeContent.node}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</DraggablePanel>
|
||||
<NavPanelDraggable activeContent={activeContent} />
|
||||
<div
|
||||
id={NAV_PANEL_RIGHT_DRAWER_ID}
|
||||
style={{
|
||||
|
||||
7
src/hooks/useTypeScriptHappyCallback.ts
Normal file
7
src/hooks/useTypeScriptHappyCallback.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { DependencyList } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useTypeScriptHappyCallback: <Args extends unknown[], R>(
|
||||
fn: (...args: Args) => R,
|
||||
deps: DependencyList,
|
||||
) => (...args: Args) => R = useCallback;
|
||||
@@ -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',
|
||||
|
||||
247
src/store/electron/actions/navigationHistory.ts
Normal file
247
src/store/electron/actions/navigationHistory.ts
Normal file
@@ -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<HistoryEntry, 'metadata'> & { metadata?: Partial<HistoryEntry['metadata']> },
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Replace current entry in history (for replace navigation)
|
||||
*/
|
||||
replaceHistory: (
|
||||
entry: Omit<HistoryEntry, 'metadata'> & { metadata?: Partial<HistoryEntry['metadata']> },
|
||||
) => 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');
|
||||
},
|
||||
});
|
||||
@@ -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<string, string>;
|
||||
@@ -29,6 +34,7 @@ export interface ElectronState {
|
||||
}
|
||||
|
||||
export const initialState: ElectronState = {
|
||||
...navigationHistoryInitialState,
|
||||
appState: {},
|
||||
dataSyncConfig: { storageMode: 'cloud' },
|
||||
desktopHotkeys: {},
|
||||
|
||||
@@ -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<ElectronStore, [['zustand/devtools', never]]> =
|
||||
...remoteSyncSlice(...parameters),
|
||||
...createElectronAppSlice(...parameters),
|
||||
...settingsSlice(...parameters),
|
||||
...createNavigationHistorySlice(...parameters),
|
||||
});
|
||||
|
||||
// =============== Implement useStore ============ //
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user