Spaces:
Sleeping
Sleeping
File size: 8,366 Bytes
dccc925 8d306f9 dccc925 8d306f9 dccc925 8d306f9 dccc925 8d306f9 dccc925 8d306f9 dccc925 8d306f9 dccc925 8d306f9 dccc925 8d306f9 dccc925 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
"""
Layout Components for Rubric AI
Contains: sidebar, base_layout, dark_mode_toggle, breadcrumbs, page_header
"""
from contextlib import contextmanager
from pathlib import Path
from nicegui import ui, app
# Get the base directory for static files (project root)
BASE_DIR = Path(__file__).parent.parent.parent
def add_head_resources():
"""Add required CSS and fonts to the page head."""
# CRITICAL: Apply sidebar state BEFORE anything else loads
# This blocking script runs synchronously before first paint
ui.add_head_html('''
<script>
(function() {
if (localStorage.getItem('sidebarCollapsed') === 'true') {
document.documentElement.classList.add('sidebar-collapsed');
}
})();
</script>
''')
# Google Fonts - Inter
ui.add_head_html('<link rel="preconnect" href="https://fonts.googleapis.com">')
ui.add_head_html('<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>')
ui.add_head_html('<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&display=swap" rel="stylesheet">')
# Material Symbols
ui.add_head_html('<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" rel="stylesheet">')
# SortableJS for drag and drop
ui.add_head_html('<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>')
# Custom CSS - use absolute path
css_path = BASE_DIR / 'static' / 'styles.css'
ui.add_css(css_path.read_text())
# Update sidebar icon after DOM loads
ui.add_head_html('''
<script>
document.addEventListener('DOMContentLoaded', function() {
const icon = document.querySelector('.sidebar-collapse-icon');
if (icon && document.documentElement.classList.contains('sidebar-collapsed')) {
icon.textContent = 'chevron_right';
}
});
</script>
''')
def material_icon(name: str, classes: str = '') -> ui.html:
"""Helper to create a Material Symbols icon."""
return ui.html(f'<span class="material-symbols-outlined {classes}">{name}</span>', sanitize=False)
def dark_mode_toggle():
"""Create a dark mode toggle button."""
async def toggle_dark_mode():
# Toggle the dark class on html element
await ui.run_javascript('''
document.documentElement.classList.toggle("dark");
return document.documentElement.classList.contains("dark");
''')
with ui.element('div').classes('dark-mode-toggle').on('click', toggle_dark_mode):
material_icon('dark_mode')
ui.label('Theme').classes('text-sm font-medium nav-item-label')
def sidebar(active_page: str = 'rubrics', on_logout=None):
"""
Create the sidebar navigation component.
Args:
active_page: Current active page ('dashboard', 'rubrics', 'templates', 'analytics', 'history')
on_logout: Callback function for logout action
"""
nav_items = [
('dashboard', 'dashboard', 'Dashboard', '/'),
('rubrics', 'edit_document', 'Rubrics', '/'),
('reference', 'grid_view', 'Reference Docs', '/reference'),
('analytics', 'bar_chart', 'Analytics', '#'),
('history', 'history', 'History', '#'),
]
bottom_items = [
('settings', 'settings', 'Settings', '#'),
('help', 'help', 'Help', '#'),
]
with ui.element('aside').classes('sidebar'):
# Top section
with ui.element('div').classes('flex flex-col gap-4'):
# Logo/Header with collapse button
with ui.element('div').classes('sidebar-header'):
ui.html('<h1 class="sidebar-title">Rubric AI</h1>', sanitize=False)
with ui.element('button').classes('sidebar-collapse-btn').on('click', lambda: ui.run_javascript('''
const html = document.documentElement;
const icon = document.querySelector('.sidebar-collapse-icon');
html.classList.toggle('sidebar-collapsed');
const isCollapsed = html.classList.contains('sidebar-collapsed');
if (isCollapsed) {
icon.textContent = 'chevron_right';
localStorage.setItem('sidebarCollapsed', 'true');
} else {
icon.textContent = 'chevron_left';
localStorage.setItem('sidebarCollapsed', 'false');
}
''')):
material_icon('chevron_left', 'sidebar-collapse-icon')
# Navigation
with ui.element('nav').classes('nav-menu'):
for item_id, icon, label, href in nav_items:
is_active = item_id == active_page
classes = 'nav-item active' if is_active else 'nav-item'
with ui.element('a').classes(classes).props(f'href="{href}"' if href != '#' else ''):
material_icon(icon)
ui.label(label).classes('text-sm font-medium nav-item-label')
# Bottom section
with ui.element('div').classes('flex flex-col gap-1'):
for item_id, icon, label, href in bottom_items:
with ui.element('a').classes('nav-item').props(f'href="{href}"' if href != '#' else ''):
material_icon(icon)
ui.label(label).classes('text-sm font-medium nav-item-label')
# Dark mode toggle
dark_mode_toggle()
# Logout (if callback provided)
if on_logout:
with ui.element('div').classes('nav-item').on('click', on_logout):
material_icon('logout')
ui.label('Logout').classes('text-sm font-medium nav-item-label')
@contextmanager
def base_layout(active_page: str = 'rubrics', on_logout=None):
"""
Context manager for the base app layout with sidebar.
Usage:
with base_layout(active_page='rubrics', on_logout=handle_logout):
# Your page content here
ui.label('Hello World')
Args:
active_page: Current active page for sidebar highlighting
on_logout: Callback function for logout action
"""
# Add required resources
add_head_resources()
# Main app shell
with ui.element('div').classes('app-shell'):
# Sidebar
sidebar(active_page=active_page, on_logout=on_logout)
# Main content area
with ui.element('main').classes('main-content'):
with ui.element('div').classes('content-wrapper') as content:
yield content
def breadcrumbs(items: list[tuple[str, str]]):
"""
Create a breadcrumb navigation.
Args:
items: List of (label, href) tuples. Last item is current page (no link).
Example:
breadcrumbs([
('Rubrics', '/'),
('History 101', '/project/123'),
('Define Prompt Groups', None) # Current page
])
"""
with ui.element('div').classes('breadcrumbs'):
for i, (label, href) in enumerate(items):
is_last = i == len(items) - 1
if is_last or href is None:
ui.label(label).classes('breadcrumb-current')
else:
ui.link(label, href).classes('breadcrumb-link')
ui.label('/').classes('breadcrumb-separator')
def page_header(title: str, subtitle: str = None, action_button: tuple = None):
"""
Create a page header with title, optional subtitle, and optional action button.
Args:
title: Main page title
subtitle: Optional subtitle/description
action_button: Optional tuple of (label, callback) for action button
"""
with ui.element('div').classes('page-header'):
with ui.element('div').classes('flex flex-col gap-2'):
ui.html(f'<p class="page-title">{title}</p>', sanitize=False)
if subtitle:
ui.html(f'<p class="page-subtitle">{subtitle}</p>', sanitize=False)
if action_button:
label, callback = action_button
ui.button(label, on_click=callback).classes('btn btn-secondary')
|