This commit is contained in:
2025-09-18 10:10:52 +02:00
parent 590af9407c
commit 994fc6873e
14 changed files with 550 additions and 136 deletions

236
CLAUDE.md
View File

@@ -3,10 +3,28 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Context
This is a NiceGUI-based web frontend for managing an Arch Linux system with AMD GPU and Ollama. The application serves as:
1. System resource monitor (CPU, GPU, memory usage)
2. Ollama model manager (list, download, delete models via Ollama API on port 11434)
3. Platform for additional system tools
This is a NiceGUI-based web platform for testing and managing AI models through Ollama on Arch Linux systems with GPU support. The application serves as an AI model testing environment featuring:
### Core Purpose
A streamlined interface for testing AI models locally, managing Ollama models, and running various AI-related testing tools.
### Main Features:
1. **Model Manager** - Complete Ollama model management interface
- Download, delete, create, and test models
- Support for Hugging Face models via Ollama pull syntax
- Rich model metadata display
- Quick in-app chat testing
2. **System Monitoring** - Resource tracking for AI workloads
- Real-time GPU monitoring (AMD/NVIDIA) to track model performance
- CPU and memory usage during model inference
- System metrics dashboard
3. **AI Testing Tools**:
- **Censor** - Text content filtering/censoring tool for testing AI outputs
- Additional testing tools to be added as needed
4. **Settings** - Application configuration and refresh intervals
## Development Commands
@@ -41,36 +59,212 @@ uv sync
- **UI Framework**: NiceGUI (async web framework based on FastAPI/Vue.js)
- **Python Version**: 3.13+
- **Ollama API**: Running on localhost:11434
- **Dependencies**:
- `nicegui` - Main UI framework
- `niceguiasyncelement` - Custom async component framework (from git)
- `psutil` - System monitoring
- `httpx` - Async HTTP client for Ollama API
- `python-dotenv` - Environment configuration
### Project Structure
- `src/main.py`: Entry point, configures NiceGUI app with environment variables
- `src/pages/`: Page components inheriting from NiceGUI elements
- `src/components/`: Reusable UI components (currently empty, ready for implementation)
- `src/static/`: Static assets (CSS, images)
- `.env`: Configuration variables (APP_TITLE, APP_PORT, APP_STORAGE_SECRET, APP_SHOW)
```
src/
├── main.py # Entry point, NiceGUI app configuration with all routes
├── pages/ # Page components (inheriting NiceGUI elements)
│ ├── dashboard.py # Main dashboard with system metrics
│ ├── ollama_manager.py # Ollama model management interface (AsyncColumn)
│ ├── system_overview.py # System information page
│ └── welcome.py # Welcome/landing page
├── components/ # Reusable UI components
│ ├── circular_progress.py # Circular progress indicators
│ ├── header.py # App header with live status
│ ├── sidebar.py # Navigation sidebar with updated menu structure
│ ├── bottom_nav.py # Mobile bottom navigation
│ ├── ollama_downloader.py # Ollama model downloader component (AsyncCard)
│ ├── ollama_model_creation.py # Model creation component (AsyncCard)
│ └── ollama_quick_test.py # Model testing component (AsyncCard)
├── utils/ # Utility modules
│ ├── gpu_monitor.py # GPU monitoring (AMD/NVIDIA auto-detect)
│ ├── system_monitor.py # System resource monitoring
│ ├── ollama_monitor.py # Ollama status monitoring (bindable dataclass)
│ └── ollama.py # Ollama API client functions
└── static/ # Static assets (CSS, images)
└── style.css # Custom dark theme styles
```
### Key Design Patterns
1. **Page Classes**: Pages are implemented as classes inheriting from NiceGUI elements (e.g., `WelcomePage` extends `ui.column`)
2. **Environment Configuration**: All app settings are managed via `.env` file and loaded with python-dotenv
3. **Async Support**: Main page handlers use async functions for potential async operations
1. **Async Components**: Uses custom `niceguiasyncelement` framework for async page/component construction
- `AsyncColumn`, `AsyncCard` base classes for complex components
- `OllamaManagerPage(AsyncColumn)` for full page async initialization
- Async component dialogs with `await component.create()` pattern
2. **Bindable Dataclasses**: Monitor classes use `@binding.bindable_dataclass` for reactive data binding
- `SystemMonitor`, `GPUMonitor`, `OllamaMonitor` for real-time data updates
3. **Environment Configuration**: All app settings are managed via `.env` file and loaded with python-dotenv
4. **Centralized Routing**: All routes defined in main.py with layout creation pattern
5. **Real-time Updates**: Timer-based updates every 2 seconds for all monitor instances
## Component Architecture
### Monitor Classes (Supporting AI Testing)
- **SystemMonitor**: Tracks system resources during AI model testing
- CPU usage during model inference
- Memory consumption by loaded models
- Disk I/O for model loading
- Process statistics for Ollama and GPU processes
- **GPUMonitor**: Critical for AI workload monitoring
- Auto-detects AMD/NVIDIA GPUs
- Tracks GPU usage during model inference
- Memory usage by loaded models
- Temperature monitoring during extended testing
- Power draw under AI workloads
- **OllamaMonitor**: Core service monitoring
- Ollama service status and version
- Currently loaded/active models
- Real-time model state tracking
### UI Components
- **MetricCircle**: Small circular progress indicator with icon
- **LargeMetricCircle**: Large circular progress for primary metrics
- **ColorfulMetricCard**: Action cards with gradient backgrounds
- **Sidebar**: Navigation menu with updated structure:
- Main: Dashboard, System Overview
- Tools: Censor (content filtering)
- Bottom: Model Manager, Settings
- **Header**: Top bar with system status indicators
### Ollama-Specific Components (AsyncCard-based):
- **OllamaDownloaderComponent**: Model downloading with progress tracking (supports HF models via Ollama's pull syntax)
- **OllamaModelCreationComponent**: Custom model creation from Modelfile
- **ModelQuickTestComponent**: Interactive model testing interface
## Ollama Integration
The Ollama API is available at `http://localhost:11434/api/`. Key endpoints:
The Ollama API client (`src/utils/ollama.py`) provides async functions:
- `status()`: Check if Ollama is online and get version
- `available_models()`: List installed models with detailed metadata
- `active_models()`: Get currently loaded/running models
- `delete_model()`: Remove a model
- `model_info()`: Get detailed model information and Modelfile
- `stream_chat()`: Stream chat responses
### AI Model Testing Features:
- **Model Discovery & Management**:
- Browse and pull models from Ollama library
- Support for HuggingFace models via Ollama syntax
- Rich metadata display (size, quantization, parameters, format)
- Time tracking for model versions
- **Testing Capabilities**:
- Quick chat interface for immediate model testing
- Model information and Modelfile inspection
- Custom model creation from Modelfiles
- Real-time resource monitoring during inference
- **Testing Tools**:
- Censor tool for output filtering analysis
- Extensible framework for adding new testing tools
API endpoints at `http://localhost:11434/api/`:
- `/api/version`: Get Ollama version
- `/api/tags`: List available models
- `/api/pull`: Download models
- `/api/delete`: Remove models
- `/api/generate`: Generate text
- `/api/chat`: Chat completion
- `/api/ps`: List running models
- `/api/show`: Show model details
## System Monitoring
For AMD GPU monitoring on Arch Linux:
- Use `rocm-smi` for GPU stats (temperature, usage, memory)
- Use `psutil` for CPU and memory monitoring
- Consider `py3nvml` or direct sysfs reading for additional GPU metrics
### GPU Monitoring Strategy
The application uses a hierarchical approach for GPU monitoring:
1. **NVIDIA GPUs** (via `nvidia-smi`):
- Temperature, usage, memory, power draw
- CUDA version and driver info
- Multi-GPU support
2. **AMD GPUs** (multiple fallbacks):
- Primary: `rocm-smi` for full metrics
- Fallback: `/sys/class/drm` filesystem
- Reads hwmon for temperature data
- Supports both server and consumer GPUs
### CPU & System Monitoring
- Real-time CPU usage and per-core statistics
- Memory (RAM and swap) usage
- Disk usage and I/O statistics
- Network traffic monitoring
- Process tracking with top processes by CPU/memory
- System uptime and kernel information
## UI/UX Features
### Dark Theme
Custom dark theme with:
- Background: `#1a1d2e` (main), `#252837` (sidebar)
- Card backgrounds: `rgba(26, 29, 46, 0.7)` with backdrop blur
- Accent colors: Cyan (`#06b6d4`) for primary actions
- Metric colors: Purple (CPU), Green (Memory), Orange (GPU), Cyan (Temp)
### Responsive Design
- Desktop: Full sidebar navigation
- Mobile: Bottom navigation bar
- Adaptive grid layouts for different screen sizes
- Viewport-aware content scaling
### Real-time Updates
- System metrics update every 2 seconds (configurable via `MONITORING_UPDATE_INTERVAL`)
- Live data binding for all metrics
- Smooth transitions and animations
## NiceGUI Patterns
- Use `ui.dark_mode()` for theme toggling
- Implement pages as classes extending NiceGUI elements for better organization
- Use `ui.timer()` for periodic updates (system stats)
- Leverage `ui.refreshable` decorator for dynamic content updates
- **Data Binding**: Use `bind_text_from()` and `bind_value_from()` for reactive updates
- **Page Routing**: Navigation via `ui.navigate.to(route)` with centralized route handling
- **Async Components**: Custom `niceguiasyncelement` framework for complex async initialization
- `AsyncColumn.create()` for async page construction
- `AsyncCard.create()` for dialog components
- `@ui.refreshable` decorators for dynamic content updates
- **Timer Updates**: `app.timer()` for periodic data refresh (2-second intervals)
- **Dialog Patterns**: Modal dialogs with `await dialog` for user interactions
- **Component Layout**: `create_layout(route)` pattern for consistent page structure
- **Dark Mode**: Forced dark mode with custom CSS overrides
## Environment Variables
Configured in `.env`:
- `MONITORING_UPDATE_INTERVAL`: Update frequency in seconds (default: 2)
- `APP_PORT`: Web server port (default: 8080, use 8081 for testing)
- `APP_TITLE`: Application title
- `APP_STORAGE_SECRET`: Session storage encryption key
- `APP_SHOW`: Auto-open browser on startup
## Testing & Development
- Run on port 8081 to avoid conflicts: `APP_PORT=8081 uv run python src/main.py`
- Monitor GPU detection in console logs
- Check Ollama connectivity at startup
- Use browser DevTools for WebSocket debugging
## Current Route Structure
From main.py routing:
- `/` - Dashboard (system metrics for monitoring AI workloads)
- `/system` - System Overview (detailed resource information)
- `/ollama` - Model Manager (primary interface for AI model testing)
- `/censor` - Censor tool (AI output filtering/testing)
- `/settings` - Settings (refresh intervals, app configuration)
### Placeholder Routes (may be repurposed for AI tools):
- `/processes` - Reserved for future AI tools
- `/network` - Reserved for future AI tools
- `/packages` - Reserved for future AI tools
- `/logs` - Reserved for future AI tools
- `/info` - Reserved for future AI tools
## Future Enhancements
- Enhanced model chat interface with conversation history
- Model performance benchmarking tools
- Batch testing capabilities for multiple models
- Output comparison tools between different models
- Integration with more AI model formats
- Advanced prompt testing and optimization tools
- Model fine-tuning interface

View File

@@ -1,4 +1,29 @@
from nicegui import ui
from utils import SystemMonitor, GPUMonitor
from typing import Optional, Literal
class MetricCircleAdv:
def __init__(self, label: str, monitor: SystemMonitor | GPUMonitor,
target_value: str,
target_max_value: str,
color: str,
formatting: Literal['percent', 'units', 'degree'],
icon: Optional[str] = None):
with ui.card().classes('metric-card p-4 text-center'):
with ui.column().classes('items-center gap-2'):
# Icon at top
with ui.row().classes('items-center gap-1'):
if icon:
ui.icon(icon, size='sm', color=color)
# Title
ui.label(label).classes('text-sm text-grey-5 font-medium')
# Circular progress - simplified
with ui.circular_progress(size='60px', color=color, show_value=False).bind_value_from(monitor, target_value):
if formatting == 'percent':
ui.label().classes('text-lg font-bold text-white').bind_text_from(monitor, target_value, backward=lambda x: f"{int(x * 100)} %")
class MetricCircle:

View File

@@ -1,17 +1,23 @@
from nicegui import ui
from utils import SystemMonitor, GPUMonitor
from nicegui import ui, binding
from utils import SystemMonitor, GPUMonitor, OllamaMonitor
class Header(ui.header):
def __init__(self, system_monitor: SystemMonitor, gpu_monitor: GPUMonitor):
def __init__(self, system_monitor: SystemMonitor, gpu_monitor: GPUMonitor, ollama_monitor: OllamaMonitor):
super().__init__(fixed=True, elevated=False)
with self.classes('bg-transparent'):
with ui.row().classes('w-full items-center justify-between px-6 py-3'):
with ui.row().classes('w-full items-center justify-between px-6'):
# Left side - minimal branding
with ui.row().classes('items-center gap-3'):
ui.label('ArchGPU Frontend').classes('text-xl font-bold text-white')
ui.chip('Live', icon='circle', color='green').props('size=sm outline')
chip = ui.chip('Live', icon='circle').props('size=sm outline')
chip.bind_text_from(ollama_monitor, 'status', backward=lambda x: self.update_ollama_running_chip(chip, x))
with ui.row().classes('items-center gap-4'):
print(ollama_monitor.active_models)
ui.label().bind_text_from(ollama_monitor, 'active_models', backward=lambda x: self.update_active_models(x))
# Right side - system status only
with ui.row().classes('items-center gap-4'):
@@ -31,3 +37,14 @@ class Header(ui.header):
ui.icon('thermostat', size='sm', color='red')
ui.label().classes('text-sm text-white').bind_text_from(gpu_monitor, 'temperature',
lambda x: f'{x:.1f}°C')
def update_ollama_running_chip(self, obj: ui.chip, state):
obj.classes(remove='text-red' if state else 'text-green')
obj.classes(add='text-green' if state else 'text-red')
return 'Ollama Running' if state else 'Ollama stopped'
def update_active_models(self, active_models):
used_vram = 0
for active_model in active_models:
used_vram += active_model['size_vram']
return f'{len(active_models)} Active Models using {(used_vram / 1024**3):.2f} GB'

View File

@@ -0,0 +1,57 @@
from nicegui import ui
from niceguiasyncelement import AsyncCard
from pathlib import Path
from utils import ollama
class OllamaDownloaderComponent(AsyncCard):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_downloading = False
self.download_progress = 0
self.download_status = ''
async def build(self) -> None:
with self:
with ui.column().classes('w-full gap-4'):
ui.label('Model Downloader').classes('text-xl font-bold')
model_input = ui.input(
'Model ID',
placeholder='e.g., TheBloke/Llama-2-7B-GGUF',
value='qwen2.5:0.5b'
).props('outlined dense').classes('w-full')
ui.link('Ollama Library', target='https://ollama.com/library/', new_tab=True)
ui.link('Using HF Models', target='https://huggingface.co/docs/hub/en/ollama', new_tab=True)
with ui.row().classes('items-center gap-2'):
ui.icon('check_circle').props(f'color=positive').bind_visibility_from(self, 'download_status', backward=lambda x: True if x == 'success' else False)
self.status_label = ui.label().bind_text_from(self, 'download_status')
ui.linear_progress(value=0, show_value=False).props('buffer=0.0 animation-speed=0').bind_value_from(self, 'download_progress')
self.download_btn = ui.button(
'Download Model',
on_click=lambda m=model_input: self.download_model(m.value) # type: ignore
).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_id', backward=lambda x: bool(x) and not self.is_downloading)
async def download_model(self, model):
self.download_btn.set_enabled(False)
try:
async for chunk in ollama.download_model(model):
if chunk.strip():
# Parse the JSON chunk and extract content
import json
try:
chunk_data = json.loads(chunk)
self.download_status = chunk_data['status']
if 'total' in chunk_data and 'completed' in chunk_data:
self.download_progress = chunk_data['completed'] / chunk_data['total']
print(self.download_progress)
else:
self.download_progress = 0
except json.JSONDecodeError:
pass # Skip malformed chunks
except Exception as e:
ui.notify(f'Error: {str(e)}', type='negative')
finally:
self.download_btn.set_enabled(True)

View File

@@ -0,0 +1,85 @@
from nicegui import ui, binding
from niceguiasyncelement import AsyncCard
from pathlib import Path
from utils import ollama
from typing import Optional, Dict
modelfile_example = """FROM qwen2.5-coder:7b
PARAMETER num_ctx 8192
PARAMETER temperature 0.1
SYSTEM "Du bist ein Python-Experte."
"""
class OllamaModelCreationComponent(AsyncCard):
model_name = binding.BindableProperty()
model_from = binding.BindableProperty()
parameters = binding.BindableProperty()
quantize = binding.BindableProperty()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_downloading = False
self.download_progress = 0
self.download_status = ''
async def build(self) -> None:
self.classes('w-full')
with self:
with ui.column().classes('w-full gap-4'):
ui.label('Create Model').classes('text-xl font-bold')
ui.input('Model Name', value='qwen2.5-coder-32k-python:latest').props('outlined dense').classes('w-full').bind_value(self, 'model_name')
ui.input('From', value='qwen2.5-coder:7b').props('outlined dense').classes('w-full').bind_value(self, 'model_from')
ui.textarea(placeholder='Parameters').classes('w-full').props('autogrow').bind_value(self, 'parameters')
ui.select(['q4_K_M', 'q4_K_S', 'q8_0'], label='quantize', clearable=True).props('outlined dense').classes('w-full').bind_value(self, 'quantize')
with ui.row().classes('items-center gap-2'):
ui.icon('check_circle').props(f'color=positive').bind_visibility_from(self, 'download_status', backward=lambda x: True if x == 'success' else False)
self.status_label = ui.label().bind_text_from(self, 'download_status')
ui.linear_progress(value=0, show_value=False).props('buffer=0.0 animation-speed=0').bind_value_from(self, 'download_progress')
self.create_btn = ui.button('Create Model', on_click=self.create_model).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_id', backward=lambda x: bool(x) and not self.is_downloading)
async def create_model(self):
self.parameters = self.parameters.strip()
model_parameters: Optional[Dict[str, str | int | float]] = None
if self.parameters:
model_parameters = {}
for line in self.parameters.split('\n'):
line = line.strip()
try:
key, value = line.split(' ')
except:
ui.notify(f'Not a valid format. {line}')
return
if key in ['num_ctx', 'repeat_last_n', 'seed', 'num_predict', 'top_k']:
model_parameters[key] = int(value)
elif key in ['repeat_penalty', 'temperature', 'top_p', 'min_p']:
model_parameters[key] = float(value)
elif key == 'stop':
model_parameters[key] = value.strip()
else:
ui.notify(f'Unknown parameter: {key}')
return
self.create_btn.set_enabled(False)
try:
async for chunk in ollama.create_ollama_model(self.model_name, self.model_from, model_parameters, self.quantize):
if chunk.strip():
# Parse the JSON chunk and extract content
import json
try:
chunk_data = json.loads(chunk)
self.download_status = chunk_data['status']
if 'total' in chunk_data and 'completed' in chunk_data:
self.download_progress = chunk_data['completed'] / chunk_data['total']
print(self.download_progress)
else:
self.download_progress = 0
except json.JSONDecodeError:
pass # Skip malformed chunks
except Exception as e:
ui.notify(f'Error: {str(e)}', type='negative')
finally:
self.create_btn.set_enabled(True)

View File

@@ -0,0 +1,65 @@
from nicegui import ui
from niceguiasyncelement import AsyncCard
from pathlib import Path
from utils import ollama
from typing import Optional
class ModelQuickTestComponent(AsyncCard):
model: Optional[str]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model = None
async def build(self, model: str) -> None:
self.model = model
with self:
with ui.card().classes('w-full'):
ui.label(f'Quick Chat Test with {model}').classes('text-h6 font-bold mb-4')
self.quick_test_textarea = ui.textarea(
label='Prompt',
placeholder='Enter your prompt here...',
value='Hello! Tell me a fun fact about AMD GPUs.'
).classes('w-full').props('autogrow outlined')
self.quick_test_send = ui.button('Send', icon='send', on_click=self._quick_test).props('color=primary')
with ui.row():
ui.icon('message', size='sm')
ui.label('Response')
self.quick_test_response = ui.label('Response will appear here...').classes('text-grey-7')
async def _quick_test(self):
if not self.model:
ui.notify('Select a model first.', type='warning')
return
self.quick_test_response.set_text('')
prompt = self.quick_test_textarea.value
# calling stream_ollama_chat
data = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"stream": True
}
self.quick_test_send.set_enabled(False)
try:
async for chunk in ollama.stream_chat(data):
if chunk.strip():
# Parse the JSON chunk and extract content
import json
try:
chunk_data = json.loads(chunk)
if 'message' in chunk_data and 'content' in chunk_data['message']:
content = chunk_data['message']['content']
current_text = self.quick_test_response.text
self.quick_test_response.set_text(current_text + content)
except json.JSONDecodeError:
pass # Skip malformed chunks
except Exception as e:
ui.notify(f'Error: {str(e)}', type='negative')
finally:
self.quick_test_send.set_enabled(True)

View File

@@ -8,29 +8,17 @@ class Sidebar:
with ui.column().classes('w-full h-full p-4'):
# Navigation sections
ui.label('MAIN').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
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('MANAGEMENT').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
with ui.column().classes('gap-1 mb-6'):
self._nav_item('Ollama Manager', 'smart_toy', '/ollama', active=(current_route == '/ollama'))
self._nav_item('Process Manager', 'terminal', '/processes', active=(current_route == '/processes'))
self._nav_item('Network Monitor', 'router', '/network', active=(current_route == '/network'))
self._nav_item('Package Manager', 'inventory_2', '/packages', active=(current_route == '/packages'))
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('Log Viewer', 'description', '/logs', active=(current_route == '/logs'))
self._nav_item('System Info', 'info', '/info', active=(current_route == '/info'))
self._nav_item('Censor', 'description', '/censor', active=(current_route == '/censor'))
ui.space()
self._nav_item('Model Manager', 'view_in_ar', '/ollama', active=(current_route == '/ollama'))
# Bottom section
ui.separator().classes('my-4')
self._nav_item('Settings', 'settings', '/settings', active=(current_route == '/settings'))

View File

@@ -5,7 +5,7 @@ from nicegui import ui, app
from components import Header, Sidebar
from pages import DashboardPage, OllamaManagerPage
from utils import GPUMonitor, SystemMonitor
from utils import GPUMonitor, SystemMonitor, OllamaMonitor
import logging
logging.basicConfig(
@@ -22,9 +22,11 @@ app.add_static_files('/static', 'src/static')
# Create monitor instances (bindable dataclasses)
system_monitor = SystemMonitor()
gpu_monitor = GPUMonitor()
ollama_monitor = OllamaMonitor()
app.timer(2.0, system_monitor.update)
app.timer(2.0, gpu_monitor.update)
app.timer(2.0, ollama_monitor.update)
def create_layout(current_route='/'):
@@ -35,7 +37,7 @@ def create_layout(current_route='/'):
# Add custom CSS
ui.add_head_html('<link rel="stylesheet" type="text/css" href="/static/style.css">')
Header(system_monitor, gpu_monitor)
Header(system_monitor, gpu_monitor, ollama_monitor)
Sidebar(current_route)

View File

@@ -1,6 +1,6 @@
from typing import Literal
from nicegui import ui
from components.circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard
from components.circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard, MetricCircleAdv
from utils import SystemMonitor, GPUMonitor
"""
@@ -58,6 +58,8 @@ class DashboardPage(ui.column):
# Main content area with proper viewport handling
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

View File

@@ -3,6 +3,9 @@ from utils import ollama
from typing import Literal, List, Dict
from pprint import pprint
from niceguiasyncelement import AsyncColumn
from components.ollama_downloader import OllamaDownloaderComponent
from components.ollama_model_creation import OllamaModelCreationComponent
from components.ollama_quick_test import ModelQuickTestComponent
class OllamaManagerPage(AsyncColumn):
@@ -44,53 +47,32 @@ class OllamaManagerPage(AsyncColumn):
with ui.row().classes('w-full items-center mb-4'):
ui.label('Installed Models').classes('text-h6 font-bold')
ui.space()
ui.button('Create New Model', icon='create', on_click=self._create_model).props('color=primary')
ui.button('Pull New Model', icon='download').props('color=primary')
ui.button('Create New Model', icon='create', on_click=self._create_model_dialog).props('color=primary')
ui.button('Pull New Model', icon='download', on_click=self._download_model_dialog).props('color=primary')
with ui.column().classes('w-full gap-2'):
await self.models_container() # type: ignore
# Quick test
with ui.card().classes('w-full'):
ui.label('Quick Chat Test').classes('text-h6 font-bold mb-4')
async def _create_model_dialog(self):
with ui.row().classes('w-full gap-2 mb-2'):
self.quick_test_select = ui.select(
[],
label='Model'
).classes('flex-grow').props('outlined')
with ui.dialog() as dialog:
await OllamaModelCreationComponent.create()
await dialog
self.models_container.refresh()
self.quick_test_textarea = ui.textarea(
label='Prompt',
placeholder='Enter your prompt here...',
value='Hello! Tell me a fun fact about AMD GPUs.'
).classes('w-full').props('outlined')
async def _download_model_dialog(self):
with ui.dialog() as dialog:
await OllamaDownloaderComponent.create()
await dialog
self.models_container.refresh()
self.quick_test_send = ui.button('Send', icon='send', on_click=self._quick_test).props('color=primary')
with ui.row():
ui.icon('message', size='sm')
ui.label('Response')
self.quick_test_response = ui.label('Response will appear here...').classes('text-grey-7')
await self._quick_test_populate_options()
async def _create_model(self):
modelfile = """FROM qwen2.5-coder:7b
PARAMETER num_ctx 32768
PARAMETER temperature 0.1
SYSTEM "Du bist ein Python-Experte."
"""
print('creating model')
result = await ollama.create_ollama_model(
"qwen2.5-coder-32k-python",
modelfile
)
print('finished.')
print(result)
await self.models_container.refresh()
async def _test_model_dialog(self, model):
with ui.dialog() as dialog:
await ModelQuickTestComponent.create(model)
await dialog
async def _loaded_models(self):
loaded = await ollama.loaded_models()
loaded = await ollama.active_models()
print(loaded)
async def _delete_model(self, model):
@@ -114,8 +96,6 @@ class OllamaManagerPage(AsyncColumn):
for model in self.models:
self._create_model_item(model)
if hasattr(self, 'quick_test_select'):
await self._quick_test_populate_options()
def _create_model_item(self, model: Dict):
with ui.card().classes('w-full'):
@@ -169,48 +149,14 @@ class OllamaManagerPage(AsyncColumn):
ui.space()
with ui.row().classes('gap-2'):
ui.button(icon='chat', on_click=lambda m=model['name']: self._test_model_dialog(m)).props('round flat').tooltip('Test Model')
ui.button(icon='play_arrow').props('round flat color=primary').tooltip('Run Model')
ui.button(icon='info', on_click=lambda m=model['name']: self._print_model_info(m)).props('round flat').tooltip('Model Info')
ui.button(icon='delete', on_click=lambda m=model['name']: self._delete_model(m)).props('round flat color=negative').tooltip('Delete Model')
async def _print_model_info(self, model_name):
result = await ollama.model_info(model_name)
print(result)
for key, value in result.items():
print(key)
async def _quick_test_populate_options(self):
select_options = [model['name'] for model in self.models]
self.quick_test_select.set_options(select_options)
async def _quick_test(self):
model = self.quick_test_select.value
if not model:
ui.notify('Select a model first.', type='warning')
return
self.quick_test_response.set_text('')
prompt = self.quick_test_textarea.value
# calling stream_ollama_chat
data = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": True
}
self.quick_test_send.set_enabled(False)
try:
async for chunk in ollama.stream_chat(data):
if chunk.strip():
# Parse the JSON chunk and extract content
import json
try:
chunk_data = json.loads(chunk)
if 'message' in chunk_data and 'content' in chunk_data['message']:
content = chunk_data['message']['content']
current_text = self.quick_test_response.text
self.quick_test_response.set_text(current_text + content)
except json.JSONDecodeError:
pass # Skip malformed chunks
except Exception as e:
ui.notify(f'Error: {str(e)}', type='negative')
finally:
self.quick_test_send.set_enabled(True)
print(result['modelfile'])

View File

@@ -168,7 +168,7 @@ body,
}
.main-content {
height: calc(100vh - 64px) !important;
height: calc(100vh - 0px) !important;
width: calc(100vw - 256px) !important;
overflow-y: auto !important;
margin-left: 256px !important;

View File

@@ -1,4 +1,5 @@
from .system_monitor import SystemMonitor
from .gpu_monitor import GPUMonitor
from .ollama_monitor import OllamaMonitor
__all__ = ['SystemMonitor', 'GPUMonitor']
__all__ = ['SystemMonitor', 'GPUMonitor', 'OllamaMonitor']

View File

@@ -1,6 +1,6 @@
import httpx
from nicegui import ui
from typing import Tuple
from typing import Tuple, Dict
async def status(url='http://127.0.0.1:11434') -> Tuple[bool, str]:
@@ -24,27 +24,30 @@ async def available_models(url='http://127.0.0.1:11434'):
return response.json()["models"]
async def loaded_models(url='http://127.0.0.1:11434'):
async def active_models(url='http://127.0.0.1:11434'):
async with httpx.AsyncClient() as client:
response = await client.get(f"{url}/api/ps")
response.raise_for_status()
return response.json()
return response.json()["models"]
async def create_ollama_model(name, modelfile_content, url='http://127.0.0.1:11434'):
async def create_ollama_model(model_name, model_from, parameters=None, quantizie=None, url='http://127.0.0.1:11434'):
data = {
"name": name,
"from": "qwen2.5-coder:7b",
"modelfile": modelfile_content,
"stream": False
"model": model_name,
"from": model_from,
"stream": True
}
if parameters:
data['parameters'] = parameters
if quantizie:
data['quantizie'] = quantizie
async with httpx.AsyncClient() as client:
response = await client.post(f"{url}/api/create", json=data)
response.raise_for_status()
print(response.text)
return response.json()
async with client.stream('POST', f"{url}/api/create", json=data) as response:
async for chunk in response.aiter_text():
yield chunk
async def delete_model(name, url='http://127.0.0.1:11434') -> bool:
@@ -79,3 +82,11 @@ async def stream_chat(data, url='http://127.0.0.1:11434'):
async with client.stream('POST', f"{url}/api/chat", json=data) as response:
async for chunk in response.aiter_text():
yield chunk
async def download_model(model, url='http://127.0.0.1:11434'):
data = {'name': model, 'stream': True}
async with httpx.AsyncClient() as client:
async with client.stream('POST', f"{url}/api/pull", json=data) as response:
async for chunk in response.aiter_text():
yield chunk

View File

@@ -0,0 +1,21 @@
import psutil
import platform
import time
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, Literal
from nicegui import binding
from utils import ollama
logger = logging.getLogger(__name__)
@binding.bindable_dataclass
class OllamaMonitor:
status: bool = False
version: str = 'Unknown'
active_models: Dict = field(default_factory=dict)
async def update(self):
self.status, self.version = await ollama.status()
self.active_models = await ollama.active_models() if self.status else {}