/**
* 侧边导航栏
*/
import { navigate, getCurrentRoute, reloadCurrentRoute } from '../router.js'
import { toggleTheme, getTheme } from '../lib/theme.js'
import { isOpenclawReady } from '../lib/app-state.js'
import { api } from '../lib/tauri-api.js'
import { toast } from './toast.js'
const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'
import { t, getLang, setLang, getAvailableLangs } from '../lib/i18n.js'
import { isFeatureAvailable } from '../lib/feature-gates.js'
import { getActiveEngine, getActiveEngineId, listEngines, switchEngine, onEngineChange } from '../lib/engine-manager.js'
function NAV_ITEMS_FULL() { return [
{
section: t('sidebar.sectionMonitor'),
items: [
{ route: '/dashboard', label: t('sidebar.dashboard'), icon: 'dashboard' },
{ route: '/assistant', label: t('sidebar.assistant'), icon: 'assistant' },
{ route: '/chat', label: t('sidebar.chat'), icon: 'chat' },
{ route: '/route-map', label: t('sidebar.routeMap'), icon: 'route-map' },
{ route: '/services', label: t('sidebar.services'), icon: 'services' },
{ route: '/logs', label: t('sidebar.logs'), icon: 'logs' },
]
},
{
section: t('sidebar.sectionConfig'),
items: [
{ route: '/models', label: t('sidebar.models'), icon: 'models' },
{ route: '/agents', label: t('sidebar.agents'), icon: 'agents' },
{ route: '/gateway', label: t('sidebar.gateway'), icon: 'gateway' },
{ route: '/channels', label: t('sidebar.channels'), icon: 'channels' },
{ route: '/communication', label: t('sidebar.communication'), icon: 'settings' },
{ route: '/security', label: t('sidebar.security'), icon: 'security' },
]
},
{
section: t('sidebar.sectionData'),
items: [
{ route: '/memory', label: t('sidebar.memory'), icon: 'memory', gate: 'memory' },
{ route: '/dreaming', label: t('sidebar.dreaming'), icon: 'dreaming', gate: 'dreaming' },
{ route: '/cron', label: t('sidebar.cron'), icon: 'clock', gate: 'cron' },
{ route: '/usage', label: t('sidebar.usage'), icon: 'bar-chart' },
]
},
{
section: t('sidebar.sectionExtension'),
items: [
{ route: '/skills', label: t('sidebar.skills'), icon: 'skills', gate: 'skills' },
{ route: '/plugin-hub', label: t('sidebar.pluginHub'), icon: 'extensions' },
]
},
{
section: '',
items: [
{ route: '/settings', label: t('sidebar.settings'), icon: 'settings' },
{ route: '/chat-debug', label: t('sidebar.checkRepair'), icon: 'diagnose' },
{ route: '/about', label: t('sidebar.about'), icon: 'about' },
]
}
] }
function NAV_ITEMS_SETUP() { return [
{
section: '',
items: [
{ route: '/setup', label: t('sidebar.setup'), icon: 'setup' },
{ route: '/assistant', label: t('sidebar.assistant'), icon: 'assistant' },
]
},
{
section: '',
items: [
{ route: '/settings', label: t('sidebar.settings'), icon: 'settings' },
{ route: '/chat-debug', label: t('sidebar.chatDebug'), icon: 'debug' },
{ route: '/about', label: t('sidebar.about'), icon: 'about' },
]
}
] }
const ICONS = {
setup: '',
dashboard: '',
chat: '',
services: '',
logs: '',
models: '',
agents: '',
gateway: '',
memory: '',
extensions: '',
about: '',
assistant: '',
security: '',
dreaming: '',
skills: '',
channels: '',
clock: '',
'bar-chart': '',
settings: '',
debug: '',
'route-map': '',
diagnose: '',
}
let _delegated = false
// === 引擎切换器 ===
function _renderEngineSwitcher() {
const engines = listEngines()
if (engines.length < 2) return '' // 只有一个引擎时不显示
const active = getActiveEngine()
if (!active) return ''
return `
${_escSidebar(t('engine.switcherSectionLabel'))}
${engines.map(e => `
${e.icon || ''}
${_escSidebar(e.name)}
${e.id === active.id ? '
' : ''}
`).join('')}
`
}
function _closeEngineDropdown() {
const dd = document.getElementById('engine-dropdown')
if (dd) dd.classList.remove('open')
const btn = document.getElementById('btn-engine-toggle')
if (btn) btn.setAttribute('aria-expanded', 'false')
}
function _toggleEngineDropdown() {
const dd = document.getElementById('engine-dropdown')
if (!dd) return
const btn = document.getElementById('btn-engine-toggle')
if (dd.classList.contains('open')) {
dd.classList.remove('open')
if (btn) btn.setAttribute('aria-expanded', 'false')
return
}
dd.classList.add('open')
if (btn) btn.setAttribute('aria-expanded', 'true')
}
const LS_SIDEBAR_COLLAPSED = 'clawpanel_sidebar_collapsed'
function _isDesktopSidebarCollapsed() {
try { return localStorage.getItem(LS_SIDEBAR_COLLAPSED) === '1' } catch { return false }
}
function _setDesktopSidebarCollapsed(collapsed) {
try { localStorage.setItem(LS_SIDEBAR_COLLAPSED, collapsed ? '1' : '0') } catch {}
const sidebar = document.getElementById('sidebar')
if (sidebar) {
sidebar.classList.toggle('sidebar-collapsed', !!collapsed)
}
const btn = document.getElementById('btn-sidebar-collapse')
if (btn) btn.textContent = collapsed ? '»' : '«'
}
export function renderSidebar(el) {
const current = getCurrentRoute()
const collapsed = _isDesktopSidebarCollapsed()
let html = `
${_renderEngineSwitcher()}
'
// 主题切换按钮
const isDark = getTheme() === 'dark'
const sunIcon = ''
const moonIcon = ''
const langCode = getLang()
const langs = getAvailableLangs()
const currentLang = langs.find(l => l.code === langCode) || langs[0]
const globeIcon = ''
const checkIcon = ''
const langOptions = langs.map(l => `
${l.label}
${l.code}
${l.code === langCode ? `${checkIcon}` : ''}
`).join('')
html += `
`
el.innerHTML = html
// 应用折叠态(桌面端)
_setDesktopSidebarCollapsed(collapsed)
// 事件委托:只绑定一次,避免重复绑定
if (!_delegated) {
_delegated = true
el.addEventListener('click', (e) => {
// 导航点击
const navItem = e.target.closest('.nav-item[data-route]')
if (navItem) {
navigate(navItem.dataset.route)
_closeMobileSidebar()
return
}
// 移动端关闭按钮
if (e.target.closest('#btn-sidebar-close')) {
_closeMobileSidebar()
return
}
// 侧边栏折叠
const collapseBtn = e.target.closest('#btn-sidebar-collapse')
if (collapseBtn) {
_setDesktopSidebarCollapsed(!_isDesktopSidebarCollapsed())
// 不需要整体重渲染
return
}
// 主题切换
const themeBtn = e.target.closest('#btn-theme-toggle')
if (themeBtn) {
toggleTheme(() => renderSidebar(el))
return
}
// 语言切换器:打开/关闭下拉
const langBtn = e.target.closest('#btn-lang-toggle')
if (langBtn) {
_toggleLangDropdown(el)
return
}
// 语言选项点击
const langOpt = e.target.closest('.lang-option[data-lang]')
if (langOpt) {
const code = langOpt.dataset.lang
if (code !== getLang()) {
setLang(code)
renderSidebar(el)
reloadCurrentRoute()
} else {
_closeLangDropdown()
}
return
}
// 引擎切换器:打开/关闭下拉
const engineBtn = e.target.closest('#btn-engine-toggle')
if (engineBtn) {
_toggleEngineDropdown()
return
}
// 引擎选项点击
const engineOpt = e.target.closest('.engine-option[data-engine]')
if (engineOpt) {
const eid = engineOpt.dataset.engine
_closeEngineDropdown()
if (eid !== getActiveEngineId()) {
engineOpt.style.opacity = '0.5'
// 立即在内容区显示加载骨架,避免切换期间空白
const contentEl = document.getElementById('content')
if (contentEl) {
contentEl.innerHTML = `
${[1,2,3].map(() => '
').join('')}
`
}
switchEngine(eid).then(() => {
toast(t('engine.switchedTo', { name: getActiveEngine()?.name || eid }), 'success')
renderSidebar(el)
// 跳转到新引擎的默认或 setup 页
const eng = getActiveEngine()
if (eng) {
navigate(eng.isReady() ? eng.getDefaultRoute() : eng.getSetupRoute())
}
}).catch(err => {
console.error('[sidebar] 切换引擎失败:', err)
toast(t('engine.switchFailed') || '引擎切换失败,请稍后重试', 'error')
renderSidebar(el)
// 恢复内容区:重新加载当前路由或显示错误占位
const contentEl = document.getElementById('content')
if (contentEl) {
const hash = window.location.hash.slice(1) || '/'
if (hash) {
reloadCurrentRoute()
} else {
contentEl.innerHTML = `加载失败,请刷新页面重试
`
}
}
})
}
return
}
// 点击其他区域关闭下拉
if (!e.target.closest('.engine-switcher')) {
_closeEngineDropdown()
}
if (!e.target.closest('.lang-switcher')) {
_closeLangDropdown()
}
})
}
}
function _escSidebar(s) { return String(s || '').replace(//g, '>') }
// === 移动端侧边栏 ===
function _closeMobileSidebar() {
const sidebar = document.getElementById('sidebar')
const overlay = document.getElementById('sidebar-overlay')
if (sidebar) sidebar.classList.remove('sidebar-open')
if (overlay) overlay.classList.remove('visible')
}
export function openMobileSidebar() {
const sidebar = document.getElementById('sidebar')
if (!sidebar) return
sidebar.classList.add('sidebar-open')
let overlay = document.getElementById('sidebar-overlay')
if (!overlay) {
overlay = document.createElement('div')
overlay.id = 'sidebar-overlay'
overlay.className = 'sidebar-overlay'
overlay.addEventListener('click', _closeMobileSidebar)
document.getElementById('app').appendChild(overlay)
}
requestAnimationFrame(() => overlay.classList.add('visible'))
}
function _closeLangDropdown() {
const sw = document.getElementById('lang-switcher')
const dd = document.getElementById('lang-dropdown')
if (dd) dd.classList.remove('open')
if (sw) sw.classList.remove('open')
}
function _toggleLangDropdown(sidebarEl) {
const sw = document.getElementById('lang-switcher')
const dd = document.getElementById('lang-dropdown')
if (!dd) return
if (dd.classList.contains('open')) { dd.classList.remove('open'); if (sw) sw.classList.remove('open'); return }
dd.classList.add('open')
if (sw) sw.classList.add('open')
const searchInput = dd.querySelector('#lang-search')
if (searchInput) {
searchInput.value = ''
_filterLangOptions('')
requestAnimationFrame(() => searchInput.focus())
searchInput.oninput = () => _filterLangOptions(searchInput.value)
}
}
function _filterLangOptions(query) {
const opts = document.querySelectorAll('#lang-options .lang-option')
const q = query.toLowerCase().trim()
opts.forEach(opt => {
if (!q) { opt.style.display = ''; return }
const label = (opt.querySelector('.lang-option-label')?.textContent || '').toLowerCase()
const code = (opt.querySelector('.lang-option-code')?.textContent || '').toLowerCase()
opt.style.display = (label.includes(q) || code.includes(q)) ? '' : 'none'
})
}