diff --git a/static/css/components/sidebar.css b/static/css/components/sidebar.css index 03a8e2c0..d4b3d9f7 100644 --- a/static/css/components/sidebar.css +++ b/static/css/components/sidebar.css @@ -184,7 +184,7 @@ padding-left: 64px; } -/* Sidebar Breadcrumb Styles */ +/* Enhanced Sidebar Breadcrumb Styles */ .sidebar-breadcrumb-container { margin-top: 8px; padding: 8px 0; @@ -211,6 +211,7 @@ cursor: pointer; transition: all 0.2s ease; color: var(--text-muted); + position: relative; } .sidebar-breadcrumb-item:hover { @@ -230,6 +231,73 @@ margin: 0 2px; } +/* New Breadcrumb Dropdown Styles */ +.breadcrumb-dropdown { + position: relative; + display: inline-flex; + align-items: center; +} + +.breadcrumb-dropdown-toggle { + margin-left: 4px; + color: inherit; + opacity: 0.7; + transition: transform 0.2s ease; +} + +.breadcrumb-dropdown:hover .breadcrumb-dropdown-toggle { + opacity: 1; +} + +.breadcrumb-dropdown.open .breadcrumb-dropdown-toggle { + transform: rotate(180deg); +} + +.breadcrumb-dropdown-menu { + position: absolute; + top: 100%; + left: 0; + min-width: 160px; + max-width: 240px; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + box-shadow: 0 3px 8px rgba(0,0,0,0.15); + z-index: calc(var(--z-overlay) + 20); + overflow-y: auto; + max-height: 260px; + display: none; + margin-top: 4px; +} + +.breadcrumb-dropdown.open .breadcrumb-dropdown-menu { + display: block; +} + +.breadcrumb-dropdown-item { + padding: 6px 12px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: all 0.2s ease; +} + +.breadcrumb-dropdown-item:hover { + background: var(--lora-surface); +} + +.breadcrumb-dropdown-item.active { + background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1); + color: var(--lora-accent); +} + +.breadcrumb-dropdown-placeholder { + color: var(--text-muted); + font-style: italic; + padding: 6px 12px; +} + /* Responsive Design */ @media (min-width: 2000px) { .folder-sidebar { diff --git a/static/css/layout.css b/static/css/layout.css index 9a5776f4..eeb76afc 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -31,7 +31,6 @@ .controls { display: flex; flex-direction: column; - gap: 8px; margin-bottom: var(--space-2); } diff --git a/static/js/checkpoints.js b/static/js/checkpoints.js index 89fab5b3..907e92a6 100644 --- a/static/js/checkpoints.js +++ b/static/js/checkpoints.js @@ -30,10 +30,6 @@ class CheckpointsPageManager { } async initialize() { - // Initialize page-specific components - this.pageControls.restoreFolderFilter(); - this.pageControls.initFolderTagsVisibility(); - // Initialize context menu new CheckpointContextMenu(); diff --git a/static/js/components/SidebarManager.js b/static/js/components/SidebarManager.js index 036985de..71910983 100644 --- a/static/js/components/SidebarManager.js +++ b/static/js/components/SidebarManager.js @@ -13,12 +13,14 @@ export class SidebarManager { this.expandedNodes = new Set(); this.isVisible = true; this.apiClient = null; + this.openDropdown = null; // Bind methods this.handleTreeClick = this.handleTreeClick.bind(this); this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this); this.toggleSidebar = this.toggleSidebar.bind(this); this.closeSidebar = this.closeSidebar.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); this.init(); } @@ -26,11 +28,19 @@ export class SidebarManager { async init() { this.apiClient = getModelApiClient(); this.setupEventHandlers(); + this.updateSidebarTitle(); this.restoreSidebarState(); await this.loadFolderTree(); this.restoreSelectedFolder(); } + updateSidebarTitle() { + const sidebarTitle = document.getElementById('sidebarTitle'); + if (sidebarTitle) { + sidebarTitle.textContent = `${this.apiClient.apiConfig.config.displayName} Root`; + } + } + setupEventHandlers() { // Sidebar toggle button const toggleBtn = document.querySelector('.sidebar-toggle-btn'); @@ -57,9 +67,9 @@ export class SidebarManager { } // Breadcrumb click handler - const breadcrumbNav = document.getElementById('breadcrumbNav'); - if (breadcrumbNav) { - breadcrumbNav.addEventListener('click', this.handleBreadcrumbClick); + const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav'); + if (sidebarBreadcrumbNav) { + sidebarBreadcrumbNav.addEventListener('click', this.handleBreadcrumbClick); } // Close sidebar when clicking outside on mobile @@ -84,6 +94,23 @@ export class SidebarManager { } } }); + + // Add document click handler for closing dropdowns + document.addEventListener('click', this.handleDocumentClick); + } + + handleDocumentClick(event) { + // Close open dropdown when clicking outside + if (this.openDropdown && !event.target.closest('.breadcrumb-dropdown')) { + this.closeDropdown(); + } + } + + closeDropdown() { + if (this.openDropdown) { + this.openDropdown.classList.remove('open'); + this.openDropdown = null; + } } async loadFolderTree() { @@ -182,7 +209,32 @@ export class SidebarManager { handleBreadcrumbClick(event) { const breadcrumbItem = event.target.closest('.sidebar-breadcrumb-item'); - if (breadcrumbItem) { + const dropdownToggle = event.target.closest('.breadcrumb-dropdown-toggle'); + const dropdownItem = event.target.closest('.breadcrumb-dropdown-item'); + + if (dropdownToggle) { + // Handle dropdown toggle + const dropdown = dropdownToggle.closest('.breadcrumb-dropdown'); + + // Close any open dropdown first + if (this.openDropdown && this.openDropdown !== dropdown) { + this.openDropdown.classList.remove('open'); + } + + // Toggle current dropdown + dropdown.classList.toggle('open'); + + // Update open dropdown reference + this.openDropdown = dropdown.classList.contains('open') ? dropdown : null; + + event.stopPropagation(); + } else if (dropdownItem) { + // Handle dropdown item selection + const path = dropdownItem.dataset.path || ''; + this.selectFolder(path); + this.closeDropdown(); + } else if (breadcrumbItem) { + // Handle direct breadcrumb click const path = breadcrumbItem.dataset.path || ''; this.selectFolder(path); } @@ -201,11 +253,8 @@ export class SidebarManager { this.pageControls.pageState.activeFolder = path || null; setStorageItem(`${this.pageType}_activeFolder`, path || null); - // Show/hide breadcrumb container - const breadcrumbContainer = document.getElementById('breadcrumbContainer'); - if (breadcrumbContainer) { - breadcrumbContainer.classList.toggle('hidden', !path); - } + // Always show breadcrumb container + // Removed hiding breadcrumb container code // Reload models with new filter await this.pageControls.resetAndReload(); @@ -251,32 +300,153 @@ export class SidebarManager { this.renderTree(); } + // Get sibling folders for a given path level + getSiblingFolders(pathParts, level) { + if (level === 0) { + // Root level siblings are top-level folders + return Object.keys(this.treeData); + } + + // Navigate to the parent folder to get siblings + let currentNode = this.treeData; + for (let i = 0; i < level; i++) { + if (!currentNode[pathParts[i]]) { + return []; + } + currentNode = currentNode[pathParts[i]]; + } + + return Object.keys(currentNode); + } + + // Get child folders for a given path + getChildFolders(path) { + if (!path) { + return Object.keys(this.treeData); + } + + const parts = path.split('/'); + let currentNode = this.treeData; + + for (const part of parts) { + if (!currentNode[part]) { + return []; + } + currentNode = currentNode[part]; + } + + return Object.keys(currentNode); + } + updateBreadcrumbs() { - const breadcrumbNav = document.getElementById('breadcrumbNav'); - if (!breadcrumbNav) return; + const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav'); + if (!sidebarBreadcrumbNav) return; const parts = this.selectedPath ? this.selectedPath.split('/') : []; let currentPath = ''; + // Start with root breadcrumb with dropdown + const rootSiblings = Object.keys(this.treeData); const breadcrumbs = [` - +
`]; + // Add separator and placeholder for next level if we're at root + if (!this.selectedPath) { + const nextLevelFolders = rootSiblings; + if (nextLevelFolders.length > 0) { + breadcrumbs.push(``); + breadcrumbs.push(` + + `); + } + } + + // Add breadcrumb items for each path segment parts.forEach((part, index) => { currentPath = currentPath ? `${currentPath}/${part}` : part; const isLast = index === parts.length - 1; + // Get siblings for this level + const siblings = this.getSiblingFolders(parts, index); + breadcrumbs.push(``); breadcrumbs.push(` - + `); + + // Add separator and placeholder for next level if not the last item + if (isLast) { + const childFolders = this.getChildFolders(currentPath); + if (childFolders.length > 0) { + breadcrumbs.push(``); + breadcrumbs.push(` + + `); + } + } }); - breadcrumbNav.innerHTML = breadcrumbs.join(''); + sidebarBreadcrumbNav.innerHTML = breadcrumbs.join(''); } updateSidebarHeader() { @@ -348,15 +518,11 @@ export class SidebarManager { this.updateTreeSelection(); this.updateBreadcrumbs(); this.updateSidebarHeader(); - - // Show breadcrumb container - const breadcrumbContainer = document.getElementById('breadcrumbContainer'); - if (breadcrumbContainer) { - breadcrumbContainer.classList.remove('hidden'); - } } else { this.updateSidebarHeader(); + this.updateBreadcrumbs(); // Always update breadcrumbs } + // Removed hidden class toggle since breadcrumbs are always visible now } saveSidebarState() { @@ -377,7 +543,7 @@ export class SidebarManager { const toggleBtn = document.querySelector('.sidebar-toggle-btn'); const closeBtn = document.getElementById('sidebarToggleClose'); const folderTree = document.getElementById('sidebarFolderTree'); - const breadcrumbNav = document.getElementById('breadcrumbNav'); + const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav'); const sidebarHeader = document.getElementById('sidebarHeader'); if (toggleBtn) { @@ -389,11 +555,14 @@ export class SidebarManager { if (folderTree) { folderTree.removeEventListener('click', this.handleTreeClick); } - if (breadcrumbNav) { - breadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick); + if (sidebarBreadcrumbNav) { + sidebarBreadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick); } if (sidebarHeader) { sidebarHeader.removeEventListener('click', () => this.selectFolder('')); } + + // Remove document click handler + document.removeEventListener('click', this.handleDocumentClick); } } diff --git a/static/js/embeddings.js b/static/js/embeddings.js index f32fd670..c2276ce8 100644 --- a/static/js/embeddings.js +++ b/static/js/embeddings.js @@ -30,10 +30,6 @@ class EmbeddingsPageManager { } async initialize() { - // Initialize page-specific components - this.pageControls.restoreFolderFilter(); - this.pageControls.initFolderTagsVisibility(); - // Initialize context menu new EmbeddingContextMenu(); diff --git a/templates/checkpoints.html b/templates/checkpoints.html index 8a205e23..64a16269 100644 --- a/templates/checkpoints.html +++ b/templates/checkpoints.html @@ -31,6 +31,7 @@ {% block content %} {% include 'components/controls.html' %} {% include 'components/duplicates_banner.html' %} + {% include 'components/folder_sidebar.html' %}