tooling and docs

This commit is contained in:
2025-09-18 22:54:40 +02:00
parent d07eb7dfd4
commit ca3ebcf02a
13 changed files with 1522 additions and 411 deletions

View File

@@ -66,14 +66,11 @@ class Header(ui.header):
value_label.bind_text_from(monitor, attr, backward=formatter)
def _format_memory(self, used: int, total: int, base=3) -> str:
print(f"{used} / {total}")
"""Format RAM usage in GB"""
if total == 0:
return "N/A"
used_gb = used / (1024**base)
total_gb = total / (1024**base)
formatted = f"{used_gb:.1f}/{total_gb:.0f}GB"
print(formatted)
return f"{used_gb:.1f}/{total_gb:.0f}GB"
def _format_disk(self, used: int, total: int) -> str:
@@ -101,18 +98,15 @@ class Header(ui.header):
if not models:
ui.label('No models loaded').classes('text-xs text-gray-400')
else:
with ui.column().classes('gap-2 p-2'):
ui.label('Active Models').classes('text-sm font-bold text-white mb-1')
for model in models:
with ui.row().classes('items-center gap-2'):
ui.icon('circle', size='xs').props('color=green')
with ui.column().classes('gap-0'):
ui.label(model.get('name', 'Unknown')).classes('text-xs text-white font-medium')
vram_gb = model.get('size_vram', 0) / (1024**3)
ui.label(f'VRAM: {vram_gb:.2f} GB').classes('text-xs text-gray-400')
if 'size' in model:
size_gb = model.get('size', 0) / (1024**3)
ui.label(f'Size: {size_gb:.2f} GB').classes('text-xs text-gray-400')
header = ['Name', 'Model', 'Context', 'Size', 'VRAM']
with ui.grid(columns=len(header)).classes('items-center gap-2 w-full'):
[ui.label(item) for item in header]
for model in ollama_monitor.active_models:
ui.label(model.get('name', 'Unknown')).classes('text-xs text-white')
ui.label(model.get('model', 'Unknown')).classes('text-xs text-white')
ui.label(f'{model.get('context_length', 'Unknown')}ctx').classes('text-xs text-white')
ui.label(f'{model.get('size', 0) / (1024**3):.1f}GB').classes('text-xs text-grey-6')
ui.label(f'{model.get('size_vram', 0) / (1024**3):.1f}GB').classes('text-xs text-grey-6')
# Display initial content
tooltip_content()

View File

@@ -1,4 +1,5 @@
from nicegui import ui
from tools import TOOLS
class Sidebar:
@@ -10,14 +11,18 @@ class Sidebar:
with ui.column().classes('gap-1 mb-6'):
self._nav_item('Dashboard', 'dashboard', '/', active=(current_route == '/'))
self._nav_item('System Overview', 'monitor', '/system', active=(current_route == '/system'))
ui.label('TOOLS').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
with ui.column().classes('gap-1 mb-6'):
self._nav_item('Censor', 'description', '/censor', active=(current_route == '/censor'))
for tool in TOOLS.values():
self._nav_item(tool.name, tool.icon, tool.baseroute, active=(current_route == tool.baseroute))
ui.space()
ui.label('EXTERNAL').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
self._nav_item_external('Open WebUI', 'view_in_ar', 'https://webui.project-insanity.de/', active=(current_route == '/ollama'))
ui.separator().classes('my-4')
self._nav_item('Model Manager', 'view_in_ar', '/ollama', active=(current_route == '/ollama'))
# Bottom section
ui.separator().classes('my-4')
@@ -31,6 +36,14 @@ class Sidebar:
text_color = 'text-cyan' if active else 'text-grey-5 hover:text-white'
icon_color = 'cyan' if active else 'grey-5'
with ui.row().classes(f'w-full items-center gap-3 px-3 py-2 rounded-lg cursor-pointer {bg_class}').on('click', navigate):
with ui.row().classes(f'w-full items-center gap-3 px-3 py-2 rounded-lg cursor-pointer {bg_class} hover:bg-cyan-600/30').on('click', navigate):
ui.icon(icon, size='sm', color=icon_color)
ui.label(label).classes(f'text-sm {text_color}')
def _nav_item_external(self, label: str, icon: str, url: str, active: bool = False):
def navigate():
ui.navigate.to(url, new_tab=True)
with ui.row().classes(f'w-full items-center gap-3 px-3 py-2 rounded-lg cursor-pointer hover:bg-cyan-600/30').on('click', navigate):
ui.icon(icon, size='sm')
ui.label(label).classes(f'text-sm')

View File

@@ -8,6 +8,9 @@ from pages import DashboardPage, OllamaManagerPage
from utils import GPUMonitor, SystemMonitor, OllamaMonitor
import logging
from tools import TOOLS
from tools.base_tool import ToolContext, set_tool_context
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
@@ -28,6 +31,14 @@ app.timer(2.0, system_monitor.update)
app.timer(2.0, gpu_monitor.update)
app.timer(2.0, ollama_monitor.update)
# Initialize tool context
tool_context = ToolContext(
system_monitor=system_monitor,
gpu_monitor=gpu_monitor,
ollama_monitor=ollama_monitor
)
set_tool_context(tool_context)
def create_layout(current_route='/'):
# Force dark mode
@@ -41,22 +52,31 @@ def create_layout(current_route='/'):
Sidebar(current_route)
# Create tool routes with sub-pages support
for tool_baseroute, tool in TOOLS.items():
# Register all routes defined by the tool
for sub_path, handler in tool.routes.items():
# Construct full route path
full_route = tool.baseroute + sub_path if sub_path else tool.baseroute
# Create a closure to capture the current handler and route
def create_route_handler(route, handler_func):
@ui.page(route)
async def tool_page():
create_layout(route)
await handler_func()
return tool_page
# Register the route
create_route_handler(full_route, handler)
@ui.page('/')
async def index_page():
create_layout('/')
DashboardPage(system_monitor, gpu_monitor, ollama_monitor)
@ui.page('/system')
async def system_page():
create_layout('/system')
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('System Overview').classes('text-2xl font-bold text-white mb-4')
with ui.card().classes('metric-card p-6'):
ui.label('Detailed system information will be displayed here...').classes('text-grey-5')
@ui.page('/ollama')
async def ollama_page():
create_layout('/ollama')
@@ -65,56 +85,6 @@ async def ollama_page():
# await page._load_models()
@ui.page('/processes')
async def processes_page():
create_layout('/processes')
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('Process Manager').classes('text-2xl font-bold text-white')
with ui.card().classes('metric-card p-6'):
ui.label('Process management coming soon...').classes('text-grey-5')
@ui.page('/network')
async def network_page():
create_layout('/network')
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('Network Monitor').classes('text-2xl font-bold text-white')
with ui.card().classes('metric-card p-6'):
ui.label('Network monitoring coming soon...').classes('text-grey-5')
@ui.page('/packages')
async def packages_page():
create_layout('/packages')
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('Package Manager').classes('text-2xl font-bold text-white')
with ui.card().classes('metric-card p-6'):
ui.label('Package management coming soon...').classes('text-grey-5')
@ui.page('/logs')
async def logs_page():
create_layout('/logs')
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('Log Viewer').classes('text-2xl font-bold text-white')
with ui.card().classes('metric-card p-6'):
ui.label('Log viewing coming soon...').classes('text-grey-5')
@ui.page('/info')
async def info_page():
create_layout('/info')
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('System Information').classes('text-2xl font-bold text-white')
with ui.card().classes('metric-card p-6'):
ui.label('Detailed system information coming soon...').classes('text-grey-5')
@ui.page('/settings')
async def settings_page():
create_layout('/settings')

View File

@@ -1,6 +1,5 @@
from .welcome import WelcomePage
from .system_overview import SystemOverviewPage
from .ollama_manager import OllamaManagerPage
from .dashboard import DashboardPage
__all__ = ['WelcomePage', 'SystemOverviewPage', 'OllamaManagerPage', 'DashboardPage']
__all__ = ['WelcomePage', 'OllamaManagerPage', 'DashboardPage']

View File

@@ -1,50 +1,7 @@
from typing import Literal
from nicegui import ui
from components.circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard, MetricCircleAdv
from utils import SystemMonitor, GPUMonitor, OllamaMonitor
"""
with ui.element('div').classes('main-content w-full'):
with ui.column().classes('w-full max-w-4xl mx-auto p-6 gap-6'):
ui.label('Ollama Manager').classes('text-2xl font-bold text-white mb-4')
# Status cards
with ui.row().classes('w-full gap-4 mb-6'):
with ui.card().classes('metric-card flex-grow p-4'):
with ui.row().classes('items-center gap-2'):
ui.icon('check_circle', color='green')
ui.label('Status: Online').classes('font-medium text-white')
with ui.card().classes('metric-card flex-grow p-4'):
ui.label('Version: 0.11.11').classes('font-medium text-white')
# Models list
with ui.card().classes('metric-card p-6'):
ui.label('Installed Models').classes('text-lg font-bold text-white mb-4')
models = [
('llama3.2:3b', '2.0 GB', 'Q4_0'),
('mistral:7b', '4.1 GB', 'Q4_0'),
('codellama:13b', '7.4 GB', 'Q4_K_M'),
('phi3:mini', '2.3 GB', 'Q4_0'),
]
for name, size, quant in models:
with ui.card().classes('metric-card p-4 mb-2'):
with ui.row().classes('w-full items-center'):
with ui.column().classes('gap-1'):
ui.label(name).classes('font-bold text-white')
with ui.row().classes('gap-2'):
ui.chip(size, icon='storage').props('outline dense color=cyan')
ui.chip(quant, icon='memory').props('outline dense color=orange')
ui.space()
with ui.row().classes('gap-2'):
ui.button(icon='play_arrow').props('round flat color=green').tooltip('Run')
ui.button(icon='info').props('round flat color=blue').tooltip('Info')
ui.button(icon='delete').props('round flat color=red').tooltip('Delete')
"""
from pprint import pprint
class DashboardPage(ui.column):
@@ -53,204 +10,291 @@ class DashboardPage(ui.column):
super().__init__(wrap=wrap, align_items=align_items)
self.system_monitor = system_monitor
self.gpu_monitor = gpu_monitor
self.ollama_monitor = ollama_monitor
self.classes('main-content w-full')
# Main content area with proper viewport handling
self.classes('main-content')
with self:
with ui.column().classes('w-full max-w-6xl mx-auto p-6 gap-6'):
with ui.grid(columns=4).classes('w-full gap-4'):
MetricCircleAdv('CPU', system_monitor, 'cpu_percent', '', icon='memory', formatting='percent', color='#e879f9')
# Top stats grid
with ui.grid(columns=4).classes('w-full gap-4'):
# CPU metric with binding
with ui.card().classes('metric-card p-4 text-center'):
with ui.column().classes('items-center gap-2'):
ui.icon('memory', size='md', color='#e879f9')
ui.label('CPU').classes('text-sm text-grey-5 font-medium')
ui.circular_progress(size='60px', color='#e879f9').bind_value_from(
system_monitor, 'cpu_percent', lambda x: x / 100)
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'cpu_percent', lambda x: f'{x:.1f}%')
# Page title
ui.label('System Monitor').classes('text-2xl font-bold text-white mb-2')
# Memory metric with binding
with ui.card().classes('metric-card p-4 text-center'):
with ui.column().classes('items-center gap-2'):
ui.icon('storage', size='md', color='#10b981')
ui.label('Memory').classes('text-sm text-grey-5 font-medium')
ui.circular_progress(size='60px', color='#10b981').bind_value_from(
system_monitor, 'memory_percent', lambda x: x / 100)
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'memory_used', lambda x: f'{x / (1024**3):.1f}GB')
# PRIMARY METRICS ROW - Large cards for critical monitoring
with ui.row().classes('w-full gap-4'):
# System Details
self._create_system_details_section()
self._create_ollama_section()
# GPU metric with conditional rendering
with ui.card().classes('metric-card p-4 text-center'):
with ui.column().classes('items-center gap-2'):
ui.icon('gpu_on', size='md', color='#f97316')
ui.label('GPU').classes('text-sm text-grey-5 font-medium')
ui.circular_progress(size='60px', color='#f97316').bind_value_from(
gpu_monitor, 'usage', lambda x: x / 100 if gpu_monitor.available else 0)
ui.label().classes('text-lg font-bold text-white').bind_text_from(
gpu_monitor, 'usage', lambda x: f'{x:.1f}%' if gpu_monitor.available else 'N/A')
with ui.row().classes('w-full gap-4'):
# GPU Section - Most important for AI workloads
self._create_gpu_section()
# Temperature metric
with ui.card().classes('metric-card p-4 text-center'):
with ui.column().classes('items-center gap-2'):
ui.icon('thermostat', size='md', color='#06b6d4')
ui.label('Temp').classes('text-sm text-grey-5 font-medium')
ui.circular_progress(size='60px', color='#06b6d4').bind_value_from(
gpu_monitor, 'temperature', lambda x: x / 100 if gpu_monitor.available else 0)
ui.label().classes('text-lg font-bold text-white').bind_text_from(
gpu_monitor, 'temperature', lambda x: f'{x:.1f}°C' if gpu_monitor.available else 'N/A')
# CPU & Memory Section
self._create_cpu_memory_section()
# Storage & Network
self._create_storage_network_section()
# Main dashboard content
with ui.row().classes('w-full gap-6'):
# Left column - charts and graphs
with ui.column().classes('flex-grow gap-4'):
# Performance chart card
with ui.card().classes('chart-area p-6'):
ui.label('System Performance').classes('text-lg font-bold text-white mb-4')
# PROCESS MONITORING
self._create_process_section()
# Simulated chart area
with ui.element('div').classes('h-48 w-full relative').style('background: linear-gradient(45deg, #1a1d2e 0%, #2a2d3e 100%); border-radius: 8px'):
# Chart lines simulation
with ui.element('svg').classes('absolute inset-0 w-full h-full'):
ui.html('''
<svg viewBox="0 0 400 200" class="w-full h-full">
<defs>
<linearGradient id="gradient1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#e879f9;stop-opacity:0.3" />
<stop offset="100%" style="stop-color:#e879f9;stop-opacity:0" />
</linearGradient>
<linearGradient id="gradient2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.3" />
<stop offset="100%" style="stop-color:#10b981;stop-opacity:0" />
</linearGradient>
</defs>
<path d="M 20 100 Q 100 50 200 80 T 380 60" stroke="#e879f9" stroke-width="2" fill="none"/>
<path d="M 20 100 Q 100 50 200 80 T 380 60 L 380 180 L 20 180 Z" fill="url(#gradient1)"/>
<path d="M 20 120 Q 100 90 200 110 T 380 100" stroke="#10b981" stroke-width="2" fill="none"/>
<path d="M 20 120 Q 100 90 200 110 T 380 100 L 380 180 L 20 180 Z" fill="url(#gradient2)"/>
</svg>
''')
def _create_gpu_section(self):
"""Create GPU monitoring section"""
with ui.card().classes('metric-card p-6 flex-1'):
ui.label('GPU Performance').classes('text-lg font-bold text-white mb-4')
# Chart legend
with ui.row().classes('gap-6 mt-4'):
with ui.row().classes('items-center gap-2'):
ui.element('div').classes('w-3 h-3 rounded-full').style('background: #e879f9')
ui.label('CPU Usage').classes('text-sm text-grey-5')
with ui.row().classes('items-center gap-2'):
ui.element('div').classes('w-3 h-3 rounded-full').style('background: #10b981')
ui.label('Memory Usage').classes('text-sm text-grey-5')
with ui.row().classes('gap-6 items-center'):
# GPU Load Circle
with ui.column().classes('items-center gap-2'):
with ui.circular_progress(size='100px', color='#f97316', show_value=False).bind_value_from(
self.gpu_monitor, 'usage', lambda x: x / 100 if self.gpu_monitor.available else 0):
ui.label().classes('text-2xl font-bold text-white').bind_text_from(
self.gpu_monitor, 'usage', lambda x: f'{x:.0f}%' if self.gpu_monitor.available else 'N/A')
ui.label('GPU Load').classes('text-xs text-grey-5 uppercase')
# Quick actions
with ui.row().classes('w-full gap-4'):
ColorfulMetricCard('Process Manager', 'terminal', '#e879f9')
ColorfulMetricCard('Network Monitor', 'router', '#10b981')
ColorfulMetricCard('Log Viewer', 'description', '#f97316')
# GPU Metrics
with ui.column().classes('flex-1 gap-3'):
# GPU Name
ui.label().classes('text-sm text-white font-medium').bind_text_from(
self.gpu_monitor, 'gpu_name', lambda x: x if self.gpu_monitor.available else 'No GPU Detected')
# Right column - system info and GPU details
with ui.column().classes('w-80 gap-4'):
# Large GPU usage circle with binding
with ui.card().classes('metric-card p-6 text-center'):
with ui.column().classes('items-center gap-3'):
ui.label('GPU Usage').classes('text-sm text-grey-5 font-medium uppercase tracking-wide')
ui.circular_progress(size='120px', color='#f97316').bind_value_from(
gpu_monitor, 'usage', lambda x: x / 100 if gpu_monitor.available else 0)
ui.label().classes('text-2xl font-bold text-white').bind_text_from(
gpu_monitor, 'usage', lambda x: f'{int(x)}%' if gpu_monitor.available else '0%')
ui.label().classes('text-xs text-grey-5').bind_text_from(
gpu_monitor, 'gpu_name', lambda x: x if gpu_monitor.available else 'No GPU Detected')
# VRAM Bar
with ui.column().classes('gap-1'):
with ui.row().classes('justify-between'):
ui.label('VRAM').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.gpu_monitor, 'memory_used',
lambda x: f'{x / 1024:.1f} / {self.gpu_monitor.memory_total / 1024:.1f} GB')
ui.linear_progress(size='4px', color='purple', show_value=False).bind_value_from(
self.gpu_monitor, 'memory_percent', lambda x: x / 100)
# System info card with bindings
with ui.card().classes('metric-card p-4'):
ui.label('System Info').classes('text-sm font-bold text-white mb-3')
# Temperature & Power
with ui.row().classes('gap-4'):
with ui.row().classes('items-center gap-2'):
ui.icon('thermostat', size='xs', color='red')
ui.label().classes('text-sm text-white').bind_text_from(
self.gpu_monitor, 'temperature', lambda x: f'{x:.0f}°C' if x > 0 else 'N/A')
with ui.column().classes('gap-2'):
# OS
with ui.row().classes('w-full justify-between'):
ui.label('OS').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white font-medium').bind_text_from(
system_monitor, 'os_name')
# Kernel
with ui.row().classes('w-full justify-between'):
ui.label('Kernel').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white font-medium').bind_text_from(
system_monitor, 'kernel')
# CPU
with ui.row().classes('w-full justify-between'):
ui.label('CPU').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white font-medium').bind_text_from(
system_monitor, 'cpu_model')
# GPU
with ui.row().classes('w-full justify-between'):
ui.label('GPU').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white font-medium').bind_text_from(
gpu_monitor, 'gpu_name', lambda x: x if gpu_monitor.available else 'No GPU')
# Uptime
with ui.row().classes('w-full justify-between'):
ui.label('Uptime').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white font-medium').bind_text_from(
system_monitor, 'uptime')
with ui.row().classes('items-center gap-2'):
ui.icon('bolt', size='xs', color='yellow')
ui.label().classes('text-sm text-white').bind_text_from(
self.gpu_monitor, 'power_draw', lambda x: f'{x:.0f}W' if x > 0 else 'N/A')
# Ollama status card
with ui.card().classes('metric-card p-4'):
ui.label('Ollama Status').classes('text-sm font-bold text-white mb-3')
def _create_cpu_memory_section(self):
"""Create CPU and Memory monitoring section"""
with ui.card().classes('metric-card p-6 flex-1'):
ui.label('CPU & Memory').classes('text-lg font-bold text-white mb-4')
with ui.row().classes('items-center gap-2 mb-2'):
ui.icon('check_circle', color='green', size='sm')
ui.label('Online').classes('text-sm text-white')
with ui.row().classes('gap-6'):
# CPU Section
with ui.column().classes('flex-1 gap-3'):
# CPU Usage Circle
with ui.column().classes('items-center gap-4'):
with ui.circular_progress(size='80px', color='#e879f9', show_value=False).bind_value_from(
self.system_monitor, 'cpu_percent', lambda x: x / 100):
ui.label().classes('text-xl font-bold text-white').bind_text_from(
self.system_monitor, 'cpu_percent', lambda x: f'{x:.1f}%')
ui.label('CPU Usage').classes('text-xs text-grey-5')
with ui.column().classes('gap-1'):
ui.label('4 models active').classes('text-xs text-grey-5')
ui.label('llama3.2:3b, mistral:7b...').classes('text-xs text-grey-6')
# CPU Details
with ui.column().classes('gap-2'):
ui.label().classes('text-xs text-grey-5').bind_text_from(
self.system_monitor, 'cpu_model', lambda x: x[:30] + '...' if len(x) > 30 else x)
with ui.row().classes('gap-3'):
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'cpu_count', lambda x: f'{x} cores')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'cpu_frequency', lambda x: f'{x:.1f} GHz' if x else 'N/A')
# Bottom metrics row with bindings
with ui.grid(columns=5).classes('w-full gap-4 mt-4'):
# Processes
with ui.card().classes('metric-card p-3 text-center'):
with ui.column().classes('items-center gap-1'):
ui.icon('dashboard', size='sm', color='grey-5')
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'process_count', lambda x: str(x))
ui.label('Processes').classes('text-xs text-grey-5')
# Memory Section
with ui.column().classes('flex-1 gap-3'):
# RAM Usage Circle
with ui.column().classes('items-center gap-4'):
with ui.circular_progress(size='80px', color='#10b981', show_value=False).bind_value_from(
self.system_monitor, 'memory_percent', lambda x: x / 100):
ui.label().classes('text-xl font-bold text-white').bind_text_from(
self.system_monitor, 'memory_percent', lambda x: f'{x:.1f}%')
ui.label('Memory').classes('text-xs text-grey-5')
# Network
with ui.card().classes('metric-card p-3 text-center'):
with ui.column().classes('items-center gap-1'):
ui.icon('wifi', size='sm', color='grey-5')
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'network_bytes_recv',
lambda x: self._format_network(system_monitor.network_bytes_recv + system_monitor.network_bytes_sent))
ui.label('Network').classes('text-xs text-grey-5')
# Memory Details
with ui.column().classes('gap-2'):
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'memory_used',
lambda x: f'{x / (1024**3):.1f} / {self.system_monitor.memory_total / (1024**3):.0f} GB')
# Disk
with ui.card().classes('metric-card p-3 text-center'):
with ui.column().classes('items-center gap-1'):
ui.icon('storage', size='sm', color='grey-5')
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'disk_percent', lambda x: f'{x:.0f}%')
ui.label('Disk').classes('text-xs text-grey-5')
# Swap if available
ui.label().classes('text-xs text-grey-5').bind_text_from(
self.system_monitor, 'swap_used',
lambda x: f'Swap: {x / (1024**3):.1f} / {self.system_monitor.swap_total / (1024**3):.0f} GB' if self.system_monitor.swap_total > 0 else '')
# CPU Cores
with ui.card().classes('metric-card p-3 text-center'):
with ui.column().classes('items-center gap-1'):
ui.icon('settings', size='sm', color='grey-5')
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'cpu_count', lambda x: str(x))
ui.label('CPU Cores').classes('text-xs text-grey-5')
def _create_ollama_section(self):
"""Create Ollama status section"""
with ui.card().classes('metric-card p-6'):
ui.label('Ollama Service').classes('text-lg font-bold text-white mb-4')
# Total RAM
with ui.card().classes('metric-card p-3 text-center'):
with ui.column().classes('items-center gap-1'):
ui.icon('memory', size='sm', color='grey-5')
ui.label().classes('text-lg font-bold text-white').bind_text_from(
system_monitor, 'memory_total', lambda x: f'{x / (1024**3):.0f}GB')
ui.label('Total RAM').classes('text-xs text-grey-5')
# Status indicator
with ui.row().classes('items-center gap-3 mb-4'):
# Status icon with conditional color
@ui.refreshable
def status_icon():
color = 'green' if self.ollama_monitor.status else 'red'
ui.icon('circle', size='sm', color=color)
status_icon()
ui.timer(2.0, status_icon.refresh)
ui.label().classes('text-sm text-white font-medium').bind_text_from(
self.ollama_monitor, 'status', lambda x: 'Online' if x else 'Offline')
ui.label().classes('text-xs text-grey-5').bind_text_from(
self.ollama_monitor, 'version', lambda x: f'v{x}' if x != 'Unknown' else '')
def _format_network(self, total_bytes: int) -> str:
"""Format network bytes to human readable format"""
mb = total_bytes / (1024 * 1024)
if mb > 1024:
return f"{mb/1024:.1f}GB"
# Active models
with ui.column().classes('gap-2'):
ui.label('Active Models').classes('text-sm text-grey-5 mb-1')
@ui.refreshable
def model_list():
if not self.ollama_monitor.active_models:
ui.label('No models loaded').classes('text-xs text-grey-6 italic')
else:
# active_models_table = ui.table(columns=columns, rows=[]).classes('w-full bg-transparent border-0 shadow-none')
header = ['Name', 'Model', 'Context', 'Size', 'VRAM']
with ui.grid(columns=len(header)).classes('items-center gap-2 w-full'):
[ui.label(item) for item in header]
for model in self.ollama_monitor.active_models:
ui.label(model.get('name', 'Unknown')).classes('text-xs text-white')
ui.label(model.get('model', 'Unknown')).classes('text-xs text-white')
ui.label(f'{model.get('context_length', 'Unknown')}ctx').classes('text-xs text-white')
ui.label(f'{model.get('size', 0) / (1024**3):.1f}GB').classes('text-xs text-grey-6')
ui.label(f'{model.get('size_vram', 0) / (1024**3):.1f}GB').classes('text-xs text-grey-6')
model_list()
ui.timer(2.0, model_list.refresh)
def _create_storage_network_section(self):
"""Create storage and network monitoring section"""
with ui.card().classes('metric-card p-6 flex-1'):
ui.label('Storage & Network').classes('text-lg font-bold text-white mb-4')
with ui.row().classes('gap-6'):
# Disk Usage
with ui.column().classes('flex-1 gap-3'):
ui.label('Primary Disk').classes('text-sm text-grey-5')
# Disk usage bar
with ui.column().classes('gap-1'):
with ui.row().classes('justify-between'):
ui.label().classes('text-sm text-white font-medium').bind_text_from(
self.system_monitor, 'disk_percent', lambda x: f'{x:.0f}% Used')
ui.label().classes('text-xs text-grey-5').bind_text_from(
self.system_monitor, 'disk_free',
lambda x: f'{x / (1024**3):.0f} GB Free')
ui.linear_progress(size='8px', color='green', show_value=False).bind_value_from(
self.system_monitor, 'disk_percent', lambda x: x / 100)
ui.label().classes('text-xs text-grey-6').bind_text_from(
self.system_monitor, 'disk_used',
lambda x: f'{x / (1024**3):.0f} / {self.system_monitor.disk_total / (1024**3):.0f} GB')
# Network I/O
with ui.column().classes('flex-1 gap-3'):
ui.label('Network I/O').classes('text-sm text-grey-5')
with ui.column().classes('gap-2'):
# Download
with ui.row().classes('items-center gap-2'):
ui.icon('download', size='xs', color='cyan')
ui.label().classes('text-sm text-white').bind_text_from(
self.system_monitor, 'network_bytes_recv',
lambda x: f'{self._format_bytes(x)}')
# Upload
with ui.row().classes('items-center gap-2'):
ui.icon('upload', size='xs', color='orange')
ui.label().classes('text-sm text-white').bind_text_from(
self.system_monitor, 'network_bytes_sent',
lambda x: f'{self._format_bytes(x)}')
def _create_system_details_section(self):
"""Create system information section"""
with ui.card().classes('metric-card p-6 w-96'):
ui.label('System Information').classes('text-lg font-bold text-white mb-4')
with ui.column().classes('gap-3'):
# OS Info
with ui.row().classes('justify-between'):
ui.label('Operating System').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'os_name')
with ui.row().classes('justify-between'):
ui.label('Kernel').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'kernel')
with ui.row().classes('justify-between'):
ui.label('Hostname').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'hostname')
with ui.row().classes('justify-between'):
ui.label('Architecture').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'architecture')
ui.separator().classes('my-2')
with ui.row().classes('justify-between'):
ui.label('Uptime').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'uptime')
with ui.row().classes('justify-between'):
ui.label('Processes').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'process_count', str)
with ui.row().classes('justify-between'):
ui.label('Load Average').classes('text-xs text-grey-5')
ui.label().classes('text-xs text-white').bind_text_from(
self.system_monitor, 'load_avg',
lambda x: f'{x[0]:.2f}, {x[1]:.2f}, {x[2]:.2f}' if x else 'N/A')
def _create_process_section(self):
"""Create process monitoring section"""
with ui.card().classes('metric-card p-6 w-full'):
ui.label('Top Processes').classes('text-lg font-bold text-white mb-4')
# Process table header
with ui.row().classes('w-full px-2 pb-2 border-b border-gray-700'):
ui.label('Process').classes('text-xs text-grey-5 font-medium w-64')
ui.label('PID').classes('text-xs text-grey-5 font-medium w-20')
ui.label('CPU %').classes('text-xs text-grey-5 font-medium w-20')
ui.label('Memory').classes('text-xs text-grey-5 font-medium w-24')
ui.label('Status').classes('text-xs text-grey-5 font-medium w-20')
# Process list
@ui.refreshable
def process_list():
processes = self.system_monitor.top_processes[:8] # Show top 8 processes
if not processes:
ui.label('No process data available').classes('text-xs text-grey-6 italic p-2')
else:
for proc in processes:
with ui.row().classes('w-full px-2 py-1 hover:bg-gray-800 hover:bg-opacity-30'):
ui.label(proc.get('name', 'Unknown')[:30]).classes('text-xs text-white w-64 truncate')
ui.label(str(proc.get('pid', 0))).classes('text-xs text-grey-5 w-20')
ui.label(f"{proc.get('cpu_percent', 0):.1f}%").classes('text-xs text-white w-20')
mem_mb = proc.get('memory_info', {}).get('rss', 0) / (1024 * 1024)
ui.label(f'{mem_mb:.0f} MB').classes('text-xs text-white w-24')
status_color = 'green' if proc.get('status') == 'running' else 'grey-5'
ui.label(proc.get('status', 'unknown')).classes(f'text-xs text-{status_color} w-20')
process_list()
ui.timer(2.0, process_list.refresh)
def _format_bytes(self, bytes_val: int) -> str:
"""Format bytes to human readable format"""
if bytes_val < 1024:
return f"{bytes_val} B"
elif bytes_val < 1024 * 1024:
return f"{bytes_val / 1024:.1f} KB"
elif bytes_val < 1024 * 1024 * 1024:
return f"{bytes_val / (1024 * 1024):.1f} MB"
else:
return f"{mb:.0f}MB"
return f"{bytes_val / (1024 * 1024 * 1024):.1f} GB"

View File

@@ -1,45 +0,0 @@
from nicegui import ui
class SystemOverviewPage(ui.column):
def __init__(self):
super().__init__()
with self.classes('w-full gap-6 p-6'):
ui.label('System Overview').classes('text-h4 font-bold')
with ui.row().classes('w-full gap-4 flex-wrap'):
self._create_stat_card('CPU Usage', '45%', 'memory', 'blue')
self._create_stat_card('Memory', '8.2 / 16 GB', 'storage', 'green')
self._create_stat_card('GPU Usage', '78%', 'gpu_on', 'orange')
self._create_stat_card('GPU Memory', '6.1 / 8 GB', 'memory_alt', 'purple')
with ui.card().classes('w-full'):
ui.label('System Information').classes('text-h6 font-bold mb-4')
with ui.grid(columns=2).classes('w-full gap-4'):
self._info_row('Operating System', 'Arch Linux')
self._info_row('Kernel', '6.16.7-arch1-1')
self._info_row('GPU', 'AMD Radeon RX 6700 XT')
self._info_row('Driver', 'amdgpu')
self._info_row('CPU', 'AMD Ryzen 7 5800X')
self._info_row('Uptime', '2 days, 14:32:15')
with ui.card().classes('w-full'):
ui.label('GPU Temperature').classes('text-h6 font-bold mb-4')
with ui.row().classes('items-center gap-4'):
ui.icon('thermostat', size='lg')
ui.label('65°C').classes('text-h5')
ui.linear_progress(value=0.65, show_value=False).classes('flex-grow')
def _create_stat_card(self, title: str, value: str, icon: str, color: str):
with ui.card().classes(f'flex-grow min-w-[200px]'):
with ui.row().classes('items-center gap-4'):
ui.icon(icon, size='lg').classes(f'text-{color}')
with ui.column().classes('gap-1'):
ui.label(title).classes('text-caption text-gray-500')
ui.label(value).classes('text-h6 font-bold')
def _info_row(self, label: str, value: str):
with ui.row().classes('w-full'):
ui.label(label).classes('font-medium')
ui.label(value).classes('text-gray-600 dark:text-gray-400')

36
src/tools/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
import os
import importlib
from typing import List, Dict
from .base_tool import BaseTool
def discover_tools() -> Dict[str, BaseTool]:
"""Auto-discover and load all tools"""
tools = {}
tools_dir = os.path.dirname(__file__)
for item in os.listdir(tools_dir):
tool_path = os.path.join(tools_dir, item)
if os.path.isdir(tool_path) and not item.startswith('_'):
try:
# Import the tool module
module = importlib.import_module(f'tools.{item}.tool')
# Find Tool class (should be named like CensorTool, etc.)
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (isinstance(attr, type) and
issubclass(attr, BaseTool) and
attr != BaseTool):
tool_instance = attr()
# Only register enabled tools
if tool_instance.enabled:
tools[tool_instance.baseroute] = tool_instance
break
except ImportError as e:
print(f"Failed to load tool {item}: {e}")
return tools
TOOLS = discover_tools()

105
src/tools/base_tool.py Normal file
View File

@@ -0,0 +1,105 @@
from abc import ABC, abstractmethod
from typing import Dict, Callable, Awaitable, Optional
from nicegui import ui
from niceguiasyncelement import AsyncColumn
import inspect
class ToolContext:
"""Global context providing access to system monitors and shared resources"""
def __init__(self, system_monitor=None, gpu_monitor=None, ollama_monitor=None):
self.system_monitor = system_monitor
self.gpu_monitor = gpu_monitor
self.ollama_monitor = ollama_monitor
# Global context instance
_tool_context: Optional[ToolContext] = None
def get_tool_context() -> ToolContext:
"""Get the global tool context"""
if _tool_context is None:
raise RuntimeError("Tool context not initialized. Call set_tool_context() first.")
return _tool_context
def set_tool_context(context: ToolContext):
"""Set the global tool context"""
global _tool_context
_tool_context = context
class BaseTool(ABC):
@property
def context(self) -> ToolContext:
"""Access to shared system monitors and resources"""
return get_tool_context()
@property
def baseroute(self) -> str:
"""Auto-generate route from module name"""
# Get the module path: tools.example_tool.tool
module = inspect.getmodule(self)
if module:
module_name = module.__name__
else:
raise ValueError("no module name specified.")
# Extract package name: example_tool
package_name = module_name.split('.')[-2]
# Convert to route: /example-tool
return f"/{package_name.replace('_', '-')}"
@property
@abstractmethod
def name(self) -> str:
"""Tool name for display"""
pass
@property
@abstractmethod
def description(self) -> str:
"""Tool description"""
pass
@property
@abstractmethod
def icon(self) -> str:
"""Material icon name"""
pass
@property
def enabled(self) -> bool:
"""Whether this tool is enabled (default: True)"""
return True
@property
@abstractmethod
def routes(self) -> Dict[str, Callable[[], Awaitable]]:
"""Define sub-routes relative to baseroute
Returns: Dict of {sub_path: handler_method}
Example: {
'': lambda: MainPage().create(self),
'/settings': lambda: SettingsPage().create(self),
'/history': lambda: HistoryPage().create(self)
}
"""
pass
class BasePage(AsyncColumn):
"""Base class for all tool pages - handles common setup"""
tool: 'BaseTool'
async def build(self, tool: 'BaseTool'):
"""Common setup for all pages"""
self.classes('main-content')
self.tool = tool
with self:
await self.content()
@abstractmethod
async def content(self):
"""Override this to provide page-specific content"""
pass

View File

View File

@@ -0,0 +1,135 @@
from typing import Dict, Callable, Awaitable
from nicegui import ui
from tools.base_tool import BaseTool, BasePage
class ExampleTool(BaseTool):
@property
def name(self) -> str:
return "Example Tool"
@property
def description(self) -> str:
return "Shows how to build a tool with multiple pages."
@property
def icon(self) -> str:
return "extension"
@property
def enabled(self) -> bool:
"""Enable/disable this tool (set to False to hide from menu and disable routes)"""
return True # Set to False to disable this tool
@property
def routes(self) -> Dict[str, Callable[[], Awaitable]]:
"""Define the routes for this tool"""
return {
'': lambda: MainPage().create(self),
'/settings': lambda: SettingsPage().create(self),
'/history': lambda: HistoryPage().create(self),
}
class MainPage(BasePage):
"""Main page of the example tool"""
async def content(self):
ui.label(self.tool.name).classes('text-2xl font-bold text-white mb-4')
# Description
with ui.card().classes('metric-card p-4'):
ui.label('Main Page').classes('text-lg font-bold text-white mb-2')
ui.label(self.tool.description).classes('text-sm text-grey-5')
# Navigation to sub-pages
ui.label('Navigate to:').classes('text-sm text-grey-5 mt-4')
with ui.row().classes('gap-2'):
ui.button('Settings', icon='settings',
on_click=lambda: ui.navigate.to(f'{self.tool.baseroute}/settings')).props('color=primary')
ui.button('History', icon='history',
on_click=lambda: ui.navigate.to(f'{self.tool.baseroute}/history')).props('color=secondary')
# Example content with context usage
with ui.card().classes('metric-card p-6 mt-4'):
ui.label('Example Content').classes('text-lg font-bold text-white mb-2')
ui.label('This is the main page of the example tool.').classes('text-sm text-grey-5')
ui.label('Tools can have multiple pages using sub-routes!').classes('text-sm text-grey-5')
# Demonstrate context access
with ui.card().classes('metric-card p-6 mt-4'):
ui.label('Context Demo').classes('text-lg font-bold text-white mb-2')
# Access system monitors through context
ui.label().classes('text-sm text-white').bind_text_from(
self.tool.context.system_monitor, 'cpu_percent',
backward=lambda x: f'CPU Usage: {x:.1f}%'
)
ui.label().classes('text-sm text-white').bind_text_from(
self.tool.context.gpu_monitor, 'temperature',
backward=lambda x: f'GPU Temperature: {x:.0f}°C' if x > 0 else 'GPU Temperature: N/A'
)
ui.label().classes('text-sm text-white').bind_text_from(
self.tool.context.ollama_monitor, 'active_models',
backward=lambda x: f'Active Models: {len(x)}'
)
class SettingsPage(BasePage):
"""Settings sub-page"""
async def content(self):
# Header with back button
with ui.row().classes('items-center gap-4 mb-4'):
ui.button(icon='arrow_back', on_click=lambda: ui.navigate.to(self.tool.baseroute)).props('flat round')
ui.label(f'{self.tool.name} - Settings').classes('text-2xl font-bold text-white')
# Settings content
with ui.card().classes('metric-card p-6'):
ui.label('Tool Settings').classes('text-lg font-bold text-white mb-4')
with ui.column().classes('gap-4'):
# Example settings
with ui.row().classes('items-center justify-between'):
ui.label('Enable feature').classes('text-sm text-white')
ui.switch(value=True).props('color=cyan')
with ui.row().classes('items-center justify-between'):
ui.label('Update interval').classes('text-sm text-white')
ui.select(['1s', '2s', '5s', '10s'], value='2s').props('outlined dense color=cyan')
with ui.row().classes('items-center justify-between'):
ui.label('Max items').classes('text-sm text-white')
ui.number(value=100, min=1, max=1000).props('outlined dense')
class HistoryPage(BasePage):
"""History sub-page"""
async def content(self):
# Header with back button
with ui.row().classes('items-center gap-4 mb-4'):
ui.button(icon='arrow_back', on_click=lambda: ui.navigate.to(self.tool.baseroute)).props('flat round')
ui.label(f'{self.tool.name} - History').classes('text-2xl font-bold text-white')
# History content
with ui.card().classes('metric-card p-6'):
ui.label('Activity History').classes('text-lg font-bold text-white mb-4')
# Example history items
history_items = [
('Action performed', '2 minutes ago', 'check_circle', 'green'),
('Settings updated', '15 minutes ago', 'settings', 'cyan'),
('Process started', '1 hour ago', 'play_arrow', 'orange'),
('Error occurred', '2 hours ago', 'error', 'red'),
]
with ui.column().classes('gap-2'):
for action, time, icon, color in history_items:
with ui.row().classes('items-center gap-3 p-2 hover:bg-gray-800 hover:bg-opacity-30 rounded'):
ui.icon(icon, size='sm', color=color)
with ui.column().classes('flex-1 gap-0'):
ui.label(action).classes('text-sm text-white')
ui.label(time).classes('text-xs text-grey-5')