Merge pull request #377 from willmiao/sidebar, See #257 #52

Sidebar
This commit is contained in:
pixelpaws
2025-08-26 19:10:30 +08:00
committed by GitHub
21 changed files with 1319 additions and 405 deletions

View File

@@ -191,7 +191,7 @@ class BaseModelRoutes(ABC):
'modelname': request.query.get('search_modelname', 'true').lower() == 'true',
'tags': request.query.get('search_tags', 'false').lower() == 'true',
'creator': request.query.get('search_creator', 'false').lower() == 'true',
'recursive': request.query.get('recursive', 'false').lower() == 'true',
'recursive': request.query.get('recursive', 'true').lower() == 'true',
}
# Parse hash filters if provided

View File

@@ -68,7 +68,7 @@ class BaseModelService(ABC):
'filename': True,
'modelname': True,
'tags': False,
'recursive': False,
'recursive': True,
}
# Get the base data set using new sort logic
@@ -139,7 +139,7 @@ class BaseModelService(ABC):
# Apply folder filtering
if folder is not None:
if search_options and search_options.get('recursive', False):
if search_options and search_options.get('recursive', True):
# Recursive folder filtering - include all subfolders
data = [
item for item in data

View File

@@ -46,7 +46,7 @@ html, body {
/* Composed Colors */
--lora-accent: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
--lora-surface: oklch(100% 0 0 / 0.98);
--lora-surface: oklch(97% 0 0 / 0.95);
--lora-border: oklch(90% 0.02 256 / 0.15);
--lora-text: oklch(95% 0.02 256);
--lora-error: oklch(75% 0.32 29);

View File

@@ -445,69 +445,6 @@
border-color: var(--lora-accent);
}
/* Switch styles */
.search-option-switch {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: var(--lora-accent);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--lora-accent);
}
input:checked + .slider:before {
transform: translateX(22px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.search-options-panel,

View File

@@ -0,0 +1,495 @@
.folder-sidebar {
position: fixed;
top: 68px; /* Below header */
left: 10px;
width: 230px;
height: calc(100vh - 88px);
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
z-index: var(--z-overlay);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
backdrop-filter: blur(8px);
/* Default state: hidden off-screen */
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
/* Visible state */
.folder-sidebar.visible {
transform: translateX(0);
opacity: 1;
pointer-events: all;
}
/* Auto-hide states */
.folder-sidebar.auto-hide {
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
.folder-sidebar.auto-hide.hover-active {
transform: translateX(0);
opacity: 1;
pointer-events: all;
}
.folder-sidebar.collapsed {
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
/* Hover detection area for auto-hide */
.sidebar-hover-area {
position: fixed;
top: 68px;
left: 0;
width: 20px;
height: calc(100vh - 88px);
z-index: calc(var(--z-overlay) - 1);
background: transparent;
pointer-events: all;
}
.sidebar-hover-area.disabled {
pointer-events: none;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg-color);
color: var(--text-color);
font-weight: 500;
font-size: 0.9em;
flex-shrink: 0;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s ease;
}
.sidebar-header:hover {
background: var(--lora-surface);
}
.sidebar-header.root-selected {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
color: var(--lora-accent);
}
.sidebar-header h3 {
margin: 0;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
flex: 1;
pointer-events: none;
}
.sidebar-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.sidebar-action-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
border-radius: 4px;
opacity: 0.6;
transition: all 0.2s ease;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-action-btn:hover {
opacity: 1;
background: var(--lora-surface);
color: var(--text-color);
}
.sidebar-action-btn.active {
opacity: 1;
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15);
color: var(--lora-accent);
}
/* Remove old close button styles */
.sidebar-toggle-close {
display: none;
}
.sidebar-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.sidebar-tree-container {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
/* Sidebar Tree Node Styles */
.sidebar-tree-node {
position: relative;
user-select: none;
}
.sidebar-tree-node-content {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85em;
border-left: 3px solid transparent;
color: var(--text-color);
}
.sidebar-tree-node-content:hover {
background: var(--lora-surface);
color: var(--text-color);
}
.sidebar-tree-node-content.selected {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
color: var(--lora-accent);
border-left-color: var(--lora-accent);
font-weight: 500;
}
.sidebar-tree-expand-icon {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 4px;
transition: transform 0.2s ease;
opacity: 0.6;
}
.sidebar-tree-expand-icon.expanded {
transform: rotate(90deg);
}
.sidebar-tree-expand-icon i {
font-size: 10px;
}
.sidebar-tree-folder-icon {
margin-right: 8px;
color: var(--text-muted);
opacity: 0.7;
}
.sidebar-tree-node-content.selected .sidebar-tree-folder-icon {
color: var(--lora-accent);
opacity: 1;
}
.sidebar-tree-node-content:hover .sidebar-tree-folder-icon {
color: var(--text-color);
opacity: 0.9;
}
.sidebar-tree-folder-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-tree-children {
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease;
}
.sidebar-tree-children.expanded {
max-height: 1000px;
}
.sidebar-tree-children .sidebar-tree-node-content {
padding-left: 32px;
}
.sidebar-tree-children .sidebar-tree-children .sidebar-tree-node-content {
padding-left: 48px;
}
.sidebar-tree-children .sidebar-tree-children .sidebar-tree-children .sidebar-tree-node-content {
padding-left: 64px;
}
/* Enhanced Sidebar Breadcrumb Styles */
.sidebar-breadcrumb-container {
margin-top: 8px;
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
background: var(--bg-color);
border-radius: var(--border-radius-xs);
}
.sidebar-breadcrumb-nav {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
font-size: 0.85em;
padding: 0 8px;
}
.sidebar-breadcrumb-item {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-muted);
position: relative;
}
.sidebar-breadcrumb-item:hover {
background: var(--lora-surface);
color: var(--text-color);
}
.sidebar-breadcrumb-item.active {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
color: var(--lora-accent);
font-weight: 500;
}
.sidebar-breadcrumb-separator {
color: var(--text-muted);
opacity: 0.6;
margin: 0 2px;
}
/* New Breadcrumb Dropdown Styles */
.breadcrumb-dropdown {
position: relative;
display: inline-flex;
align-items: center;
}
.breadcrumb-dropdown-indicator {
margin-left: 6px;
color: inherit;
opacity: 0.6;
transition: all 0.2s ease;
pointer-events: none;
font-size: 0.9em;
}
.sidebar-breadcrumb-item:hover .breadcrumb-dropdown-indicator {
opacity: 0.9;
}
.sidebar-breadcrumb-item.placeholder {
color: var(--text-muted);
font-style: italic;
}
.sidebar-breadcrumb-item.placeholder:hover {
background: var(--lora-surface);
color: var(--text-color);
}
.breadcrumb-dropdown.open .breadcrumb-dropdown-indicator {
transform: rotate(180deg);
opacity: 1;
}
.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: 450px;
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);
font-weight: 500;
}
/* Responsive Design */
@media (min-width: 2000px) {
.folder-sidebar {
width: 280px;
left: 24px;
}
}
@media (min-width: 3000px) {
.folder-sidebar {
width: 320px;
left: 32px;
}
}
@media (max-width: 1400px) {
.folder-sidebar {
width: 260px;
left: 16px;
}
}
/* Empty State */
.sidebar-tree-placeholder {
padding: 24px 16px;
text-align: center;
color: var(--text-muted);
opacity: 0.7;
}
.sidebar-tree-placeholder i {
font-size: 2em;
opacity: 0.5;
margin-bottom: 8px;
display: block;
}
/* Smooth transitions for tree nodes */
.sidebar-tree-node {
overflow: hidden;
}
.sidebar-tree-children {
transition: max-height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-tree-expand-icon {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Visual separator for nested levels */
.sidebar-tree-children .sidebar-tree-node-content {
position: relative;
}
.sidebar-tree-children .sidebar-tree-node-content::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background: var(--border-color);
opacity: 0.3;
}
/* Responsive Design */
@media (max-width: 1024px) {
.folder-sidebar {
top: 68px;
left: 16px;
width: calc(100vw - 32px);
max-width: 320px;
height: calc(100vh - 88px);
z-index: calc(var(--z-overlay) + 10);
}
.folder-sidebar.collapsed {
transform: translateX(-100%);
}
/* Mobile overlay */
.folder-sidebar:not(.collapsed)::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: -1;
backdrop-filter: blur(2px);
}
}
@media (max-width: 768px) {
.folder-sidebar {
width: calc(100vw - 32px);
max-width: 280px;
left: 16px;
}
.sidebar-breadcrumb-nav {
font-size: 0.8em;
}
.sidebar-breadcrumb-item {
padding: 3px 6px;
}
}
/* Hide scrollbar but keep functionality */
.sidebar-tree-container::-webkit-scrollbar {
width: 6px;
}
.sidebar-tree-container::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-tree-container::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.sidebar-tree-container::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}

View File

@@ -31,7 +31,6 @@
.controls {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: var(--space-2);
}
@@ -225,63 +224,6 @@
display: none !important;
}
.folder-tags-container {
position: relative;
width: 100%;
margin-bottom: 8px; /* Add margin to ensure space for the button */
}
.folder-tags {
display: flex;
gap: 4px;
padding: 2px 0;
flex-wrap: wrap;
transition: max-height 0.3s ease, opacity 0.2s ease;
max-height: 150px; /* Limit height to prevent overflow */
opacity: 1;
overflow-y: auto; /* Enable vertical scrolling */
margin-bottom: 5px; /* Add margin below the tags */
}
.folder-tags.collapsed {
max-height: 0;
opacity: 0;
margin: 0;
padding-bottom: 0;
overflow: hidden;
}
.toggle-folders-container {
margin-left: auto;
}
/* Toggle Folders Button */
.toggle-folders-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.toggle-folders-btn:hover {
background: var(--lora-accent);
color: white;
transform: translateY(-2px);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
.toggle-folders-btn i {
transition: transform 0.3s ease;
}
/* Icon-only button style */
.icon-only {
min-width: unset !important;
@@ -290,55 +232,6 @@
height: 32px !important;
}
/* Rotate icon when folders are collapsed */
.folder-tags.collapsed ~ .actions .toggle-folders-btn i {
transform: rotate(180deg);
}
/* Add custom scrollbar for better visibility */
.folder-tags::-webkit-scrollbar {
width: 6px;
}
.folder-tags::-webkit-scrollbar-track {
background: var(--card-bg);
border-radius: 3px;
}
.folder-tags::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.folder-tags::-webkit-scrollbar-thumb:hover {
background: var(--lora-accent);
}
.tag {
cursor: pointer;
padding: 2px 8px;
margin: 2px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
display: inline-block;
line-height: 1.2;
font-size: 14px;
background-color: var(--card-bg);
transition: all 0.2s ease;
}
.tag:hover {
border-color: var(--lora-accent);
background-color: oklch(var(--lora-accent) / 0.1);
transform: translateY(-1px);
}
.tag.active {
background-color: var(--lora-accent);
color: white;
border-color: var(--lora-accent);
}
/* Back to Top Button */
.back-to-top {
position: fixed;
@@ -376,10 +269,8 @@
}
/* Prevent text selection in control and header areas */
.tag,
.control-group button,
.control-group select,
.toggle-folders-btn,
.bulk-operations-panel,
.app-header,
.header-branding,
@@ -387,8 +278,7 @@
.main-nav,
.nav-item,
.header-actions button,
.header-controls,
.toggle-folders-container button {
.header-controls {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -472,18 +362,6 @@
justify-content: flex-end;
margin-top: 8px;
}
.toggle-folders-container {
margin-left: 0;
}
.folder-tags-container {
order: -1;
}
.toggle-folders-btn:hover {
transform: none; /* Disable hover effects on mobile */
}
.control-group button:hover {
transform: none; /* Disable hover effects on mobile */
@@ -493,10 +371,6 @@
transform: none; /* Disable hover effects on mobile */
}
.tag:hover {
transform: none; /* Disable hover effects on mobile */
}
.back-to-top {
bottom: 60px; /* Give some extra space from bottom on mobile */
}

View File

@@ -34,10 +34,10 @@
@import 'components/filter-indicator.css';
@import 'components/initialization.css';
@import 'components/progress-panel.css';
@import 'components/alphabet-bar.css'; /* Add alphabet bar component */
@import 'components/duplicates.css'; /* Add duplicates component */
@import 'components/keyboard-nav.css'; /* Add keyboard navigation component */
@import 'components/statistics.css'; /* Add statistics component */
@import 'components/sidebar.css'; /* Add sidebar component */
.initialization-notice {
display: flex;

View File

@@ -671,7 +671,6 @@ export class BaseModelApiClient {
if (pageState.searchOptions.creator !== undefined) {
params.append('search_creator', pageState.searchOptions.creator.toString());
}
params.append('recursive', (pageState.searchOptions?.recursive ?? false).toString());
}
}

View File

@@ -30,10 +30,6 @@ class CheckpointsPageManager {
}
async initialize() {
// Initialize page-specific components
this.pageControls.restoreFolderFilter();
this.pageControls.initFolderTagsVisibility();
// Initialize context menu
new CheckpointContextMenu();

View File

@@ -0,0 +1,729 @@
/**
* SidebarManager - Manages hierarchical folder navigation sidebar
*/
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import { getModelApiClient } from '../api/modelApiFactory.js';
export class SidebarManager {
constructor(pageControls) {
this.pageControls = pageControls;
this.pageType = pageControls.pageType;
this.treeData = {};
this.selectedPath = '';
this.expandedNodes = new Set();
this.isVisible = true;
this.isPinned = false;
this.apiClient = null;
this.openDropdown = null;
this.hoverTimeout = null;
this.isHovering = false;
// Bind methods
this.handleTreeClick = this.handleTreeClick.bind(this);
this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this);
this.handleDocumentClick = this.handleDocumentClick.bind(this);
this.handleSidebarHeaderClick = this.handleSidebarHeaderClick.bind(this);
this.handlePinToggle = this.handlePinToggle.bind(this);
this.handleCollapseAll = this.handleCollapseAll.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleHoverAreaEnter = this.handleHoverAreaEnter.bind(this);
this.handleHoverAreaLeave = this.handleHoverAreaLeave.bind(this);
this.init();
}
async init() {
this.apiClient = getModelApiClient();
// Set initial sidebar state immediately (hidden by default)
this.setInitialSidebarState();
this.setupEventHandlers();
this.updateSidebarTitle();
this.restoreSidebarState();
await this.loadFolderTree();
this.restoreSelectedFolder();
// Apply final state with animation after everything is loaded
this.applyFinalSidebarState();
}
setInitialSidebarState() {
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (!sidebar || !hoverArea) return;
// Get stored pin state
const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false);
this.isPinned = isPinned;
// Sidebar starts hidden by default (CSS handles this)
// Just set up the hover area state
if (window.innerWidth <= 1024) {
hoverArea.classList.add('disabled');
} else if (this.isPinned) {
hoverArea.classList.add('disabled');
} else {
hoverArea.classList.remove('disabled');
}
}
applyFinalSidebarState() {
// Use requestAnimationFrame to ensure DOM is ready
requestAnimationFrame(() => {
this.updateAutoHideState();
});
}
updateSidebarTitle() {
const sidebarTitle = document.getElementById('sidebarTitle');
if (sidebarTitle) {
sidebarTitle.textContent = `${this.apiClient.apiConfig.config.displayName} Root`;
}
}
setupEventHandlers() {
// Sidebar header (root selection) - only trigger on title area
const sidebarHeader = document.getElementById('sidebarHeader');
if (sidebarHeader) {
sidebarHeader.addEventListener('click', this.handleSidebarHeaderClick);
}
// Pin toggle button
const pinToggleBtn = document.getElementById('sidebarPinToggle');
if (pinToggleBtn) {
pinToggleBtn.addEventListener('click', this.handlePinToggle);
}
// Collapse all button
const collapseAllBtn = document.getElementById('sidebarCollapseAll');
if (collapseAllBtn) {
collapseAllBtn.addEventListener('click', this.handleCollapseAll);
}
// Tree click handler
const folderTree = document.getElementById('sidebarFolderTree');
if (folderTree) {
folderTree.addEventListener('click', this.handleTreeClick);
}
// Breadcrumb click handler
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
if (sidebarBreadcrumbNav) {
sidebarBreadcrumbNav.addEventListener('click', this.handleBreadcrumbClick);
}
// Hover detection for auto-hide
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (sidebar) {
sidebar.addEventListener('mouseenter', this.handleMouseEnter);
sidebar.addEventListener('mouseleave', this.handleMouseLeave);
}
if (hoverArea) {
hoverArea.addEventListener('mouseenter', this.handleHoverAreaEnter);
hoverArea.addEventListener('mouseleave', this.handleHoverAreaLeave);
}
// Close sidebar when clicking outside on mobile
document.addEventListener('click', (e) => {
if (window.innerWidth <= 1024 && this.isVisible) {
const sidebar = document.getElementById('folderSidebar');
if (sidebar && !sidebar.contains(e.target)) {
this.hideSidebar();
}
}
});
// Handle window resize
window.addEventListener('resize', () => {
this.updateAutoHideState();
});
// 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();
}
}
handleSidebarHeaderClick(event) {
// Only trigger root selection if clicking on the title area, not the buttons
if (!event.target.closest('.sidebar-header-actions')) {
this.selectFolder('');
}
}
handlePinToggle(event) {
event.stopPropagation();
this.isPinned = !this.isPinned;
this.updateAutoHideState();
this.updatePinButton();
this.saveSidebarState();
}
handleCollapseAll(event) {
event.stopPropagation();
this.expandedNodes.clear();
this.renderTree();
this.saveExpandedState();
}
handleMouseEnter() {
this.isHovering = true;
if (this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
this.hoverTimeout = null;
}
if (!this.isPinned) {
this.showSidebar();
}
}
handleMouseLeave() {
this.isHovering = false;
if (!this.isPinned) {
this.hoverTimeout = setTimeout(() => {
if (!this.isHovering) {
this.hideSidebar();
}
}, 300);
}
}
handleHoverAreaEnter() {
if (!this.isPinned) {
this.showSidebar();
}
}
handleHoverAreaLeave() {
// Let the sidebar's mouse leave handler deal with hiding
}
showSidebar() {
const sidebar = document.getElementById('folderSidebar');
if (sidebar && !this.isPinned) {
sidebar.classList.add('hover-active');
this.isVisible = true;
}
}
hideSidebar() {
const sidebar = document.getElementById('folderSidebar');
if (sidebar && !this.isPinned) {
sidebar.classList.remove('hover-active');
this.isVisible = false;
}
}
updateAutoHideState() {
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (!sidebar || !hoverArea) return;
if (window.innerWidth <= 1024) {
// Mobile: always use collapsed state
sidebar.classList.remove('auto-hide', 'hover-active', 'visible');
sidebar.classList.add('collapsed');
hoverArea.classList.add('disabled');
this.isVisible = false;
} else if (this.isPinned) {
// Desktop pinned: always visible
sidebar.classList.remove('auto-hide', 'collapsed', 'hover-active');
sidebar.classList.add('visible');
hoverArea.classList.add('disabled');
this.isVisible = true;
} else {
// Desktop auto-hide: use hover detection
sidebar.classList.remove('collapsed', 'visible');
sidebar.classList.add('auto-hide');
hoverArea.classList.remove('disabled');
if (this.isHovering) {
sidebar.classList.add('hover-active');
this.isVisible = true;
} else {
sidebar.classList.remove('hover-active');
this.isVisible = false;
}
}
}
updatePinButton() {
const pinBtn = document.getElementById('sidebarPinToggle');
if (pinBtn) {
pinBtn.classList.toggle('active', this.isPinned);
pinBtn.title = this.isPinned ? 'Unpin Sidebar' : 'Pin Sidebar';
}
}
async loadFolderTree() {
try {
const response = await this.apiClient.fetchUnifiedFolderTree();
this.treeData = response.tree || {};
this.renderTree();
} catch (error) {
console.error('Failed to load folder tree:', error);
this.renderEmptyState();
}
}
renderTree() {
const folderTree = document.getElementById('sidebarFolderTree');
if (!folderTree) return;
if (!this.treeData || Object.keys(this.treeData).length === 0) {
this.renderEmptyState();
return;
}
folderTree.innerHTML = this.renderTreeNode(this.treeData, '');
}
renderTreeNode(nodeData, basePath) {
const entries = Object.entries(nodeData);
if (entries.length === 0) return '';
return entries.map(([folderName, children]) => {
const currentPath = basePath ? `${basePath}/${folderName}` : folderName;
const hasChildren = Object.keys(children).length > 0;
const isExpanded = this.expandedNodes.has(currentPath);
const isSelected = this.selectedPath === currentPath;
return `
<div class="sidebar-tree-node" data-path="${currentPath}">
<div class="sidebar-tree-node-content ${isSelected ? 'selected' : ''}">
<div class="sidebar-tree-expand-icon ${isExpanded ? 'expanded' : ''}"
style="${hasChildren ? '' : 'opacity: 0; pointer-events: none;'}">
<i class="fas fa-chevron-right"></i>
</div>
<i class="fas fa-folder sidebar-tree-folder-icon"></i>
<div class="sidebar-tree-folder-name" title="${folderName}">${folderName}</div>
</div>
${hasChildren ? `
<div class="sidebar-tree-children ${isExpanded ? 'expanded' : ''}">
${this.renderTreeNode(children, currentPath)}
</div>
` : ''}
</div>
`;
}).join('');
}
renderEmptyState() {
const folderTree = document.getElementById('sidebarFolderTree');
if (!folderTree) return;
folderTree.innerHTML = `
<div class="sidebar-tree-placeholder">
<i class="fas fa-folder-open"></i>
<div>No folders found</div>
</div>
`;
}
handleTreeClick(event) {
const expandIcon = event.target.closest('.sidebar-tree-expand-icon');
const nodeContent = event.target.closest('.sidebar-tree-node-content');
if (expandIcon) {
// Toggle expand/collapse
const treeNode = expandIcon.closest('.sidebar-tree-node');
const path = treeNode.dataset.path;
const children = treeNode.querySelector('.sidebar-tree-children');
if (this.expandedNodes.has(path)) {
this.expandedNodes.delete(path);
expandIcon.classList.remove('expanded');
if (children) children.classList.remove('expanded');
} else {
this.expandedNodes.add(path);
expandIcon.classList.add('expanded');
if (children) children.classList.add('expanded');
}
this.saveExpandedState();
} else if (nodeContent) {
// Select folder
const treeNode = nodeContent.closest('.sidebar-tree-node');
const path = treeNode.dataset.path;
this.selectFolder(path);
}
}
handleBreadcrumbClick(event) {
const breadcrumbItem = event.target.closest('.sidebar-breadcrumb-item');
const dropdownItem = event.target.closest('.breadcrumb-dropdown-item');
if (dropdownItem) {
// Handle dropdown item selection
const path = dropdownItem.dataset.path || '';
this.selectFolder(path);
this.closeDropdown();
} else if (breadcrumbItem) {
// Handle breadcrumb item click
const path = breadcrumbItem.dataset.path || '';
const isPlaceholder = breadcrumbItem.classList.contains('placeholder');
const isActive = breadcrumbItem.classList.contains('active');
const dropdown = breadcrumbItem.closest('.breadcrumb-dropdown');
if (isPlaceholder || (isActive && path === this.selectedPath)) {
// Open dropdown for placeholders or active items
// 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;
} else {
// Navigate to the selected path
this.selectFolder(path);
}
}
}
async selectFolder(path) {
// Update selected path
this.selectedPath = path;
// Update UI
this.updateTreeSelection();
this.updateBreadcrumbs();
this.updateSidebarHeader();
// Update page state
this.pageControls.pageState.activeFolder = path || null;
setStorageItem(`${this.pageType}_activeFolder`, path || null);
// Reload models with new filter
await this.pageControls.resetAndReload();
// Auto-hide sidebar on mobile after selection
if (window.innerWidth <= 1024) {
this.hideSidebar();
}
}
updateTreeSelection() {
const folderTree = document.getElementById('sidebarFolderTree');
if (!folderTree) return;
// Remove all selections
folderTree.querySelectorAll('.sidebar-tree-node-content').forEach(node => {
node.classList.remove('selected');
});
// Add selection to current path
if (this.selectedPath) {
const selectedNode = folderTree.querySelector(`[data-path="${this.selectedPath}"] .sidebar-tree-node-content`);
if (selectedNode) {
selectedNode.classList.add('selected');
// Expand parents to show selection
this.expandPathParents(this.selectedPath);
}
}
}
expandPathParents(path) {
if (!path) return;
const parts = path.split('/');
let currentPath = '';
for (let i = 0; i < parts.length - 1; i++) {
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
this.expandedNodes.add(currentPath);
}
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 sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
if (!sidebarBreadcrumbNav) return;
const parts = this.selectedPath ? this.selectedPath.split('/') : [];
let currentPath = '';
// Start with root breadcrumb
const rootSiblings = Object.keys(this.treeData);
const breadcrumbs = [`
<div class="breadcrumb-dropdown">
<span class="sidebar-breadcrumb-item ${!this.selectedPath ? 'active' : ''}" data-path="">
<i class="fas fa-home"></i> ${this.apiClient.apiConfig.config.displayName} root
</span>
</div>
`];
// Add separator and placeholder for next level if we're at root
if (!this.selectedPath) {
const nextLevelFolders = rootSiblings;
if (nextLevelFolders.length > 0) {
breadcrumbs.push(`<span class="sidebar-breadcrumb-separator">/</span>`);
breadcrumbs.push(`
<div class="breadcrumb-dropdown">
<span class="sidebar-breadcrumb-item placeholder">
--
<span class="breadcrumb-dropdown-indicator">
<i class="fas fa-caret-down"></i>
</span>
</span>
<div class="breadcrumb-dropdown-menu">
${nextLevelFolders.map(folder => `
<div class="breadcrumb-dropdown-item" data-path="${folder}">
${folder}
</div>`).join('')
}
</div>
</div>
`);
}
}
// 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(`<span class="sidebar-breadcrumb-separator">/</span>`);
breadcrumbs.push(`
<div class="breadcrumb-dropdown">
<span class="sidebar-breadcrumb-item ${isLast ? 'active' : ''}" data-path="${currentPath}">
${part}
${siblings.length > 1 ? `
<span class="breadcrumb-dropdown-indicator">
<i class="fas fa-caret-down"></i>
</span>
` : ''}
</span>
${siblings.length > 1 ? `
<div class="breadcrumb-dropdown-menu">
${siblings.map(folder => `
<div class="breadcrumb-dropdown-item ${folder === part ? 'active' : ''}"
data-path="${currentPath.replace(part, folder)}">
${folder}
</div>`).join('')
}
</div>
` : ''}
</div>
`);
// 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(`<span class="sidebar-breadcrumb-separator">/</span>`);
breadcrumbs.push(`
<div class="breadcrumb-dropdown">
<span class="sidebar-breadcrumb-item placeholder">
--
<span class="breadcrumb-dropdown-indicator">
<i class="fas fa-caret-down"></i>
</span>
</span>
<div class="breadcrumb-dropdown-menu">
${childFolders.map(folder => `
<div class="breadcrumb-dropdown-item" data-path="${currentPath}/${folder}">
${folder}
</div>`).join('')
}
</div>
</div>
`);
}
}
});
sidebarBreadcrumbNav.innerHTML = breadcrumbs.join('');
}
updateSidebarHeader() {
const sidebarHeader = document.getElementById('sidebarHeader');
if (!sidebarHeader) return;
if (!this.selectedPath) {
sidebarHeader.classList.add('root-selected');
} else {
sidebarHeader.classList.remove('root-selected');
}
}
toggleSidebar() {
const sidebar = document.getElementById('folderSidebar');
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
if (!sidebar) return;
this.isVisible = !this.isVisible;
if (this.isVisible) {
sidebar.classList.remove('collapsed');
sidebar.classList.add('visible');
} else {
sidebar.classList.remove('visible');
sidebar.classList.add('collapsed');
}
if (toggleBtn) {
toggleBtn.classList.toggle('active', this.isVisible);
}
this.saveSidebarState();
}
closeSidebar() {
const sidebar = document.getElementById('folderSidebar');
const toggleBtn = document.querySelector('.sidebar-toggle-btn');
if (!sidebar) return;
this.isVisible = false;
sidebar.classList.remove('visible');
sidebar.classList.add('collapsed');
if (toggleBtn) {
toggleBtn.classList.remove('active');
}
this.saveSidebarState();
}
restoreSidebarState() {
const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false);
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
this.isPinned = isPinned;
this.expandedNodes = new Set(expandedPaths);
this.updatePinButton();
}
restoreSelectedFolder() {
const activeFolder = getStorageItem(`${this.pageType}_activeFolder`);
if (activeFolder) {
this.selectedPath = activeFolder;
this.updateTreeSelection();
this.updateBreadcrumbs();
this.updateSidebarHeader();
} else {
this.updateSidebarHeader();
this.updateBreadcrumbs(); // Always update breadcrumbs
}
// Removed hidden class toggle since breadcrumbs are always visible now
}
saveSidebarState() {
setStorageItem(`${this.pageType}_sidebarPinned`, this.isPinned);
}
saveExpandedState() {
setStorageItem(`${this.pageType}_expandedNodes`, Array.from(this.expandedNodes));
}
async refresh() {
await this.loadFolderTree();
this.restoreSelectedFolder();
}
destroy() {
// Clear any pending timeouts
if (this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
}
// Clean up event handlers
const pinToggleBtn = document.getElementById('sidebarPinToggle');
const collapseAllBtn = document.getElementById('sidebarCollapseAll');
const folderTree = document.getElementById('sidebarFolderTree');
const sidebarBreadcrumbNav = document.getElementById('sidebarBreadcrumbNav');
const sidebarHeader = document.getElementById('sidebarHeader');
const sidebar = document.getElementById('folderSidebar');
const hoverArea = document.getElementById('sidebarHoverArea');
if (pinToggleBtn) {
pinToggleBtn.removeEventListener('click', this.handlePinToggle);
}
if (collapseAllBtn) {
collapseAllBtn.removeEventListener('click', this.handleCollapseAll);
}
if (folderTree) {
folderTree.removeEventListener('click', this.handleTreeClick);
}
if (sidebarBreadcrumbNav) {
sidebarBreadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick);
}
if (sidebarHeader) {
sidebarHeader.removeEventListener('click', this.handleSidebarHeaderClick);
}
if (sidebar) {
sidebar.removeEventListener('mouseenter', this.handleMouseEnter);
sidebar.removeEventListener('mouseleave', this.handleMouseLeave);
}
if (hoverArea) {
hoverArea.removeEventListener('mouseenter', this.handleHoverAreaEnter);
hoverArea.removeEventListener('mouseleave', this.handleHoverAreaLeave);
}
// Remove document click handler
document.removeEventListener('click', this.handleDocumentClick);
}
}

View File

@@ -2,6 +2,7 @@
import { getCurrentPageState, setCurrentPageType } from '../../state/index.js';
import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js';
import { showToast } from '../../utils/uiHelpers.js';
import { SidebarManager } from '../SidebarManager.js';
/**
* PageControls class - Unified control management for model pages
@@ -23,6 +24,9 @@ export class PageControls {
// Store API methods
this.api = null;
// Initialize sidebar manager
this.sidebarManager = null;
// Initialize event listeners
this.initEventListeners();
@@ -55,6 +59,21 @@ export class PageControls {
registerAPI(api) {
this.api = api;
console.log(`API methods registered for ${this.pageType} page`);
// Initialize sidebar manager after API is registered
this.initSidebarManager();
}
/**
* Initialize sidebar manager
*/
async initSidebarManager() {
try {
this.sidebarManager = new SidebarManager(this);
console.log('SidebarManager initialized');
} catch (error) {
console.error('Failed to initialize SidebarManager:', error);
}
}
/**
@@ -72,17 +91,6 @@ export class PageControls {
});
}
// Use event delegation for folder tags - this is the key fix
const folderTagsContainer = document.querySelector('.folder-tags-container');
if (folderTagsContainer) {
folderTagsContainer.addEventListener('click', (e) => {
const tag = e.target.closest('.tag');
if (tag) {
this.handleFolderClick(tag);
}
});
}
// Refresh button handler
const refreshBtn = document.querySelector('[data-action="refresh"]');
if (refreshBtn) {
@@ -92,12 +100,6 @@ export class PageControls {
// Initialize dropdown functionality
this.initDropdowns();
// Toggle folders button
const toggleFoldersBtn = document.querySelector('.toggle-folders-btn');
if (toggleFoldersBtn) {
toggleFoldersBtn.addEventListener('click', () => this.toggleFolderTags());
}
// Clear custom filter handler
const clearFilterBtn = document.querySelector('.clear-filter');
if (clearFilterBtn) {
@@ -199,130 +201,6 @@ export class PageControls {
}
}
/**
* Toggle folder selection
* @param {HTMLElement} tagElement - The folder tag element that was clicked
*/
handleFolderClick(tagElement) {
const folder = tagElement.dataset.folder;
const wasActive = tagElement.classList.contains('active');
document.querySelectorAll('.folder-tags .tag').forEach(t => {
t.classList.remove('active');
});
if (!wasActive) {
tagElement.classList.add('active');
this.pageState.activeFolder = folder;
setStorageItem(`${this.pageType}_activeFolder`, folder);
} else {
this.pageState.activeFolder = null;
setStorageItem(`${this.pageType}_activeFolder`, null);
}
this.resetAndReload();
}
/**
* Restore folder filter from storage
*/
restoreFolderFilter() {
const activeFolder = getStorageItem(`${this.pageType}_activeFolder`);
const folderTag = activeFolder && document.querySelector(`.tag[data-folder="${activeFolder}"]`);
if (folderTag) {
folderTag.classList.add('active');
this.pageState.activeFolder = activeFolder;
this.filterByFolder(activeFolder);
}
}
/**
* Filter displayed cards by folder
* @param {string} folderPath - Folder path to filter by
*/
filterByFolder(folderPath) {
const cardSelector = this.pageType === 'loras' ? '.model-card' : '.checkpoint-card';
document.querySelectorAll(cardSelector).forEach(card => {
card.style.display = card.dataset.folder === folderPath ? '' : 'none';
});
}
/**
* Update the folder tags display with new folder list
* @param {Array} folders - List of folder names
*/
updateFolderTags(folders) {
const folderTagsContainer = document.querySelector('.folder-tags');
if (!folderTagsContainer) return;
// Keep track of currently selected folder
const currentFolder = this.pageState.activeFolder;
// Create HTML for folder tags
const tagsHTML = folders.map(folder => {
const isActive = folder === currentFolder;
return `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
}).join('');
// Update the container
folderTagsContainer.innerHTML = tagsHTML;
// Scroll active folder into view (no need to reattach click handlers)
const activeTag = folderTagsContainer.querySelector(`.tag[data-folder="${currentFolder}"]`);
if (activeTag) {
activeTag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
/**
* Toggle visibility of folder tags
*/
toggleFolderTags() {
const folderTags = document.querySelector('.folder-tags');
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (folderTags) {
folderTags.classList.toggle('collapsed');
if (folderTags.classList.contains('collapsed')) {
// Change icon to indicate folders are hidden
toggleBtn.className = 'fas fa-folder-plus';
toggleBtn.parentElement.title = 'Show folder tags';
setStorageItem('folderTagsCollapsed', 'true');
} else {
// Change icon to indicate folders are visible
toggleBtn.className = 'fas fa-folder-minus';
toggleBtn.parentElement.title = 'Hide folder tags';
setStorageItem('folderTagsCollapsed', 'false');
}
}
}
/**
* Initialize folder tags visibility based on stored preference
*/
initFolderTagsVisibility() {
const isCollapsed = getStorageItem('folderTagsCollapsed');
if (isCollapsed) {
const folderTags = document.querySelector('.folder-tags');
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (folderTags) {
folderTags.classList.add('collapsed');
}
if (toggleBtn) {
toggleBtn.className = 'fas fa-folder-plus';
toggleBtn.parentElement.title = 'Show folder tags';
}
} else {
const toggleBtn = document.querySelector('.toggle-folders-btn i');
if (toggleBtn) {
toggleBtn.className = 'fas fa-folder-minus';
toggleBtn.parentElement.title = 'Hide folder tags';
}
}
}
/**
* Load sort preference from storage
*/
@@ -408,6 +286,11 @@ export class PageControls {
try {
await this.api.resetAndReload(updateFolders);
// Refresh sidebar after reload if folders were updated
if (updateFolders && this.sidebarManager) {
await this.sidebarManager.refresh();
}
} catch (error) {
console.error(`Error reloading ${this.pageType}:`, error);
showToast(`Failed to reload ${this.pageType}: ${error.message}`, 'error');
@@ -426,6 +309,11 @@ export class PageControls {
try {
await this.api.refreshModels(fullRebuild);
// Refresh sidebar after rebuild
if (this.sidebarManager) {
await this.sidebarManager.refresh();
}
} catch (error) {
console.error(`Error ${fullRebuild ? 'rebuilding' : 'refreshing'} ${this.pageType}:`, error);
showToast(`Failed to ${fullRebuild ? 'rebuild' : 'refresh'} ${this.pageType}: ${error.message}`, 'error');
@@ -547,4 +435,13 @@ export class PageControls {
console.error('Model duplicates manager not available');
}
}
/**
* Clean up resources
*/
destroy() {
if (this.sidebarManager) {
this.sidebarManager.destroy();
}
}
}

View File

@@ -30,10 +30,6 @@ class EmbeddingsPageManager {
}
async initialize() {
// Initialize page-specific components
this.pageControls.restoreFolderFilter();
this.pageControls.initFolderTagsVisibility();
// Initialize context menu
new EmbeddingContextMenu();

View File

@@ -38,8 +38,6 @@ class LoraPageManager {
async initialize() {
// Initialize page-specific components
this.pageControls.restoreFolderFilter();
this.pageControls.initFolderTagsVisibility();
new LoraContextMenu();
// Initialize cards for current bulk mode state (should be false initially)

View File

@@ -19,7 +19,6 @@ export class SearchManager {
this.searchOptionsPanel = document.getElementById('searchOptionsPanel');
this.closeSearchOptions = document.getElementById('closeSearchOptions');
this.searchOptionTags = document.querySelectorAll('.search-option-tag');
this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle');
this.searchTimeout = null;
this.currentPage = options.page || document.body.dataset.page || 'loras';
@@ -112,14 +111,6 @@ export class SearchManager {
});
}
// Recursive search toggle
if (this.recursiveSearchToggle) {
this.recursiveSearchToggle.addEventListener('change', () => {
this.saveSearchPreferences();
this.performSearch();
});
}
// Add global click handler to close panels when clicking outside
document.addEventListener('click', (e) => {
// Close search options panel when clicking outside
@@ -218,11 +209,6 @@ export class SearchManager {
});
}
// Apply recursive search - only if the toggle exists
if (this.recursiveSearchToggle && preferences.recursive !== undefined) {
this.recursiveSearchToggle.checked = preferences.recursive;
}
// Ensure at least one search option is selected
this.validateSearchOptions();
} catch (error) {
@@ -272,11 +258,6 @@ export class SearchManager {
options
};
// Only add recursive option if the toggle exists
if (this.recursiveSearchToggle) {
preferences.recursive = this.recursiveSearchToggle.checked;
}
setStorageItem(`${this.currentPage}_search_prefs`, preferences);
} catch (error) {
console.error('Error saving search preferences:', error);
@@ -294,7 +275,6 @@ export class SearchManager {
performSearch() {
const query = this.searchInput.value.trim();
const options = this.getActiveSearchOptions();
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
// Update the state with search parameters
const pageState = getCurrentPageState();
@@ -318,16 +298,14 @@ export class SearchManager {
filename: options.filename || false,
modelname: options.modelname || false,
tags: options.tags || false,
creator: options.creator || false,
recursive: recursive
creator: options.creator || false
};
} else if (this.currentPage === 'checkpoints') {
pageState.searchOptions = {
filename: options.filename || false,
modelname: options.modelname || false,
tags: options.tags || false,
creator: options.creator || false,
recursive: recursive
creator: options.creator || false
};
}
}

View File

@@ -29,7 +29,7 @@ export const state = {
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: null,
activeFolder: getStorageItem(`${MODEL_TYPES.LORA}_activeFolder`),
activeLetterFilter: null,
previewVersions: loraPreviewVersions,
searchManager: null,
@@ -38,7 +38,7 @@ export const state = {
modelname: true,
tags: false,
creator: false,
recursive: false
recursive: true,
},
filters: {
baseModel: [],
@@ -78,14 +78,14 @@ export const state = {
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: null,
activeFolder: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_activeFolder`),
previewVersions: checkpointPreviewVersions,
searchManager: null,
searchOptions: {
filename: true,
modelname: true,
creator: false,
recursive: false
recursive: true,
},
filters: {
baseModel: [],
@@ -104,7 +104,7 @@ export const state = {
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: null,
activeFolder: getStorageItem(`${MODEL_TYPES.EMBEDDING}_activeFolder`),
activeLetterFilter: null,
previewVersions: embeddingPreviewVersions,
searchManager: null,
@@ -113,7 +113,7 @@ export const state = {
modelname: true,
tags: false,
creator: false,
recursive: false
recursive: true,
},
filters: {
baseModel: [],

View File

@@ -31,6 +31,7 @@
{% block content %}
{% include 'components/controls.html' %}
{% include 'components/duplicates_banner.html' %}
{% include 'components/folder_sidebar.html' %}
<!-- Checkpoint cards container -->
<div class="card-grid" id="modelGrid">
@@ -38,6 +39,10 @@
</div>
{% endblock %}
{% block overlay %}
<div class="bulk-mode-overlay"></div>
{% endblock %}
{% block main_script %}
<script type="module" src="/loras_static/js/checkpoints.js"></script>
{% endblock %}

View File

@@ -1,14 +1,4 @@
<div class="controls">
{% if folders|length > 1 %}
<div class="folder-tags-container">
<div class="folder-tags">
{% for folder in folders %}
<div class="tag" data-folder="{{ folder }}">{{ folder }}</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="actions">
<div class="action-buttons">
<div title="Sort models by..." class="control-group">
@@ -75,12 +65,6 @@
</div>
<div class="controls-right">
<div class="toggle-folders-container">
<button class="toggle-folders-btn icon-only" title="Toggle folder tags">
<i class="fas fa-tags"></i>
</button>
</div>
<div class="keyboard-nav-hint tooltip">
<i class="fas fa-keyboard"></i>
<span class="tooltiptext">
@@ -107,6 +91,13 @@
</div>
</div>
</div>
<!-- Breadcrumb Navigation -->
<div id="breadcrumbContainer" class="sidebar-breadcrumb-container">
<nav class="sidebar-breadcrumb-nav" id="sidebarBreadcrumbNav">
<!-- Breadcrumbs will be populated by JavaScript -->
</nav>
</div>
</div>
<!-- Add bulk operations panel (initially hidden) -->

View File

@@ -0,0 +1,24 @@
<!-- Hover detection area -->
<div class="sidebar-hover-area" id="sidebarHoverArea"></div>
<!-- Folder Navigation Sidebar -->
<div class="folder-sidebar" id="folderSidebar">
<div class="sidebar-header" id="sidebarHeader">
<h3><i class="fas fa-home"></i> <span id="sidebarTitle">Model Root</span></h3>
<div class="sidebar-header-actions">
<button class="sidebar-action-btn" id="sidebarCollapseAll" title="Collapse All Folders">
<i class="fas fa-compress-alt"></i>
</button>
<button class="sidebar-action-btn" id="sidebarPinToggle" title="Pin/Unpin Sidebar">
<i class="fas fa-thumbtack"></i>
</button>
</div>
</div>
<div class="sidebar-content">
<div class="sidebar-tree-container">
<div class="sidebar-tree" id="sidebarFolderTree">
<!-- Tree will be populated by JavaScript -->
</div>
</div>
</div>
</div>

View File

@@ -101,17 +101,6 @@
{% endif %}
</div>
</div>
{% if request.path != '/loras/recipes' %}
<div class="options-section">
<div class="search-option-switch">
<span>Include Subfolders</span>
<label class="switch">
<input type="checkbox" id="recursiveSearchToggle">
<span class="slider round"></span>
</label>
</div>
</div>
{% endif %}
</div>
<!-- Add filter panel -->

View File

@@ -31,6 +31,7 @@
{% block content %}
{% include 'components/controls.html' %}
{% include 'components/duplicates_banner.html' %}
{% include 'components/folder_sidebar.html' %}
<!-- Embedding cards container -->
<div class="card-grid" id="modelGrid">
@@ -38,6 +39,10 @@
</div>
{% endblock %}
{% block overlay %}
<div class="bulk-mode-overlay"></div>
{% endblock %}
{% block main_script %}
<script type="module" src="/loras_static/js/embeddings.js"></script>
{% endblock %}

View File

@@ -15,13 +15,14 @@
{% block content %}
{% include 'components/controls.html' %}
{% include 'components/alphabet_bar.html' %}
{% include 'components/duplicates_banner.html' %}
{% include 'components/folder_sidebar.html' %}
<!-- Lora卡片容器 -->
<div class="card-grid" id="modelGrid">
<!-- Cards will be dynamically inserted here -->
</div>
<!-- Bulk operations panel will be inserted here by JavaScript -->
{% endblock %}