updated readme.

model info added as dialog
This commit is contained in:
2025-09-18 23:55:47 +02:00
parent df8c672816
commit 2143074fde
6 changed files with 174 additions and 92 deletions

View File

@@ -98,7 +98,7 @@ class MyTool(BaseTool):
@property @property
def routes(self): def routes(self):
return {'': lambda: MainPage().create(self)} return {'': lambda: MainPage.create(self)}
class MainPage(BasePage): class MainPage(BasePage):
async def content(self): async def content(self):

View File

@@ -49,7 +49,7 @@ class MyTool(BaseTool):
@property @property
def routes(self) -> Dict[str, Callable[[], Awaitable]]: def routes(self) -> Dict[str, Callable[[], Awaitable]]:
return { return {
'': lambda: MainPage().create(self), '': lambda: MainPage.create(self),
} }
class MainPage(BasePage): class MainPage(BasePage):
@@ -116,10 +116,10 @@ Tools can have multiple pages using sub-routes:
@property @property
def routes(self): def routes(self):
return { return {
'': lambda: MainPage().create(self), # /my-tool '': lambda: MainPage.create(self), # /my-tool
'/settings': lambda: SettingsPage().create(self), # /my-tool/settings '/settings': lambda: SettingsPage.create(self), # /my-tool/settings
'/results': lambda: ResultsPage().create(self), # /my-tool/results '/results': lambda: ResultsPage.create(self), # /my-tool/results
'/history': lambda: HistoryPage().create(self), # /my-tool/history '/history': lambda: HistoryPage.create(self), # /my-tool/history
} }
``` ```

View File

@@ -0,0 +1,155 @@
from nicegui import ui
from niceguiasyncelement import AsyncCard
from pathlib import Path
from utils import ollama
from typing import Optional
class ModelInfoComponent(AsyncCard):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
async def build(self, model_info: dict) -> None:
# self.classes('max-w-1/2')
self.style('width: 1200px; max-width: none')
with self:
# Header with model name
with ui.row().classes('w-full items-center justify-between p-4 border-b border-gray-700'):
ui.label('Model Information').classes('text-xl font-bold text-white')
# Tabs for better navigation
with ui.tabs().classes('w-full') as tabs:
# Determine which tabs to show based on available data
has_basic = any(key in model_info for key in ['license', 'template', 'system'])
has_params = 'parameters' in model_info and model_info['parameters']
has_details = 'details' in model_info and model_info['details']
has_modelfile = 'modelfile' in model_info and model_info['modelfile']
has_technical = 'model_info' in model_info and isinstance(model_info['model_info'], dict)
has_raw = bool(model_info) # Always show raw tab if we have any data
# Initialize tab variables
basic_tab = None
params_tab = None
details_tab = None
modelfile_tab = None
technical_tab = None
raw_tab = None
if has_basic:
basic_tab = ui.tab('Basic', icon='info')
if has_params:
params_tab = ui.tab('Parameters', icon='tune')
if has_details:
details_tab = ui.tab('Details', icon='memory')
if has_modelfile:
modelfile_tab = ui.tab('Modelfile', icon='code')
if has_technical:
technical_tab = ui.tab('Technical', icon='settings')
if has_raw:
raw_tab = ui.tab('Raw', icon='data_object')
with ui.tab_panels(tabs, value=basic_tab if basic_tab else (params_tab if params_tab else None)).classes('w-full h-full bg-transparent'):
# Basic Information Tab
if has_basic:
with ui.tab_panel(basic_tab).classes('p-4'):
with ui.scroll_area().classes('w-full').style('height: 400px; max-height: 60vh'):
with ui.column().classes('gap-4'):
if 'license' in model_info:
with ui.row().classes('items-start gap-4'):
ui.label('License:').classes('text-sm font-bold text-white min-w-24')
ui.label(model_info['license']).classes('text-sm text-grey-5 flex-1')
if 'template' in model_info:
ui.label('Template:').classes('text-sm font-bold text-white mb-2')
ui.html(f'<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-3 rounded border max-w-full overflow-x-auto">{model_info["template"]}</pre>').classes('w-full')
if 'system' in model_info:
ui.label('System Prompt:').classes('text-sm font-bold text-white mb-2')
ui.html(f'<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-3 rounded border max-w-full overflow-x-auto">{model_info["system"]}</pre>').classes('w-full')
# Parameters Tab
if has_params and params_tab:
with ui.tab_panel(params_tab).classes('p-4'):
with ui.column().classes('gap-2'):
# Parse parameters string into key-value pairs
params_text = model_info['parameters']
if isinstance(params_text, str):
# Split by lines and parse each parameter
lines = params_text.strip().split('\n')
for line in lines:
if line.strip():
# Split on first whitespace sequence to separate key from value
parts = line.strip().split(None, 1)
if len(parts) == 2:
param, value = parts
with ui.row().classes('items-center justify-between p-2 hover:bg-gray-800 hover:bg-opacity-30 rounded'):
ui.label(param.replace('_', ' ').title()).classes('text-sm text-white font-medium')
ui.label(str(value)).classes('text-sm text-grey-5 font-mono')
else:
# Fallback for dict format if it exists
for param, value in params_text.items():
with ui.row().classes('items-center justify-between p-2 hover:bg-gray-800 hover:bg-opacity-30 rounded'):
ui.label(param.replace('_', ' ').title()).classes('text-sm text-white font-medium')
ui.label(str(value)).classes('text-sm text-grey-5 font-mono')
# Details Tab
if has_details and details_tab:
with ui.tab_panel(details_tab).classes('p-4'):
with ui.column().classes('gap-2'):
details = model_info['details']
detail_items = [
('Format', details.get('format')),
('Family', details.get('family')),
('Families', ', '.join(details.get('families', [])) if details.get('families') else None),
('Parameter Size', details.get('parameter_size')),
('Quantization Level', details.get('quantization_level')),
]
for label, value in detail_items:
if value:
with ui.row().classes('items-center justify-between p-2 hover:bg-gray-800 hover:bg-opacity-30 rounded'):
ui.label(label).classes('text-sm text-white font-medium')
ui.label(str(value)).classes('text-sm text-grey-5')
# Modelfile Tab
if has_modelfile and modelfile_tab:
with ui.tab_panel(modelfile_tab).classes('p-4'):
with ui.column().classes('gap-4 w-full'):
with ui.row().classes('w-full justify-end'):
ui.button('Copy to Clipboard', icon='content_copy',
on_click=lambda: ui.run_javascript(f'navigator.clipboard.writeText(`{model_info["modelfile"].replace("`", "\\`")}`)')).props('size=sm color=primary')
ui.html(f'''
<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-4 rounded border max-w-full overflow-x-auto" style="max-height: 400px; overflow-y: auto;">{model_info["modelfile"]}</pre>
''').classes('w-full')
# Technical Details Tab
if has_technical and technical_tab:
with ui.tab_panel(technical_tab).classes('p-4'):
with ui.column().classes('gap-2'):
info = model_info['model_info']
for key, value in info.items():
if key.startswith('general.'):
display_key = key.replace('general.', '').replace('_', ' ').title()
with ui.row().classes('items-center justify-between p-2 hover:bg-gray-800 hover:bg-opacity-30 rounded'):
ui.label(display_key).classes('text-sm text-white font-medium')
ui.label(str(value)).classes('text-sm text-grey-5 font-mono')
# Raw Data Tab
if has_raw and raw_tab:
with ui.tab_panel(raw_tab).classes('p-4'):
with ui.column().classes('gap-4 w-full'):
with ui.row().classes('w-full justify-end'):
ui.button('Copy to Clipboard', icon='content_copy',
on_click=lambda: ui.run_javascript(f'''
navigator.clipboard.writeText(`{str(model_info).replace("`", "\\`")}`)
''')).props('size=sm color=primary')
# Pretty print the entire model_info dictionary
import pprint
pretty_text = pprint.pformat(model_info, indent=2, width=100, depth=None)
ui.html(f'''
<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-4 rounded border max-w-full overflow-x-auto" style="max-height: 400px; overflow-y: auto;">{pretty_text}</pre>
''').classes('w-full')

View File

@@ -1,82 +0,0 @@
#!/usr/bin/env python3
"""
Example of using the refactored monitoring classes with NiceGUI's reactive system.
This demonstrates how the bindable dataclasses automatically update the UI.
"""
from nicegui import ui, app
from utils import GPUMonitor, SystemMonitor
# Create monitor instances (bindable dataclasses)
system_monitor = SystemMonitor()
gpu_monitor = GPUMonitor()
app.timer(2.0, system_monitor.update)
app.timer(2.0, gpu_monitor.update)
@ui.page('/')
async def index_page():
"""Example usage of monitoring classes with NiceGUI"""
# Create UI that automatically updates when dataclass fields change
with ui.card().classes('w-full'):
ui.label('System Monitor').classes('text-h4')
# CPU section - binds directly to dataclass fields
with ui.row():
ui.label('CPU:')
ui.label().bind_text_from(system_monitor, 'cpu_percent',
lambda x: f'{x:.1f}%')
ui.label().bind_text_from(system_monitor, 'cpu_model')
# Memory section
with ui.row():
ui.label('Memory:')
ui.label().bind_text_from(system_monitor, 'memory_percent',
lambda x: f'{x:.1f}%')
ui.label().bind_text_from(system_monitor, 'memory_used',
lambda x: f'{x / (1024**3):.1f} GB used')
# Disk section
with ui.row():
ui.label('Disk:')
ui.label().bind_text_from(system_monitor, 'disk_percent',
lambda x: f'{x:.1f}%')
# Process count
with ui.row():
ui.label('Processes:')
ui.label().bind_text_from(system_monitor, 'process_count')
# GPU Monitor section (if available)
if gpu_monitor.available:
with ui.card().classes('w-full mt-4'):
ui.label('GPU Monitor').classes('text-h4')
with ui.row():
ui.label('GPU:')
ui.label().bind_text_from(gpu_monitor, 'gpu_name')
ui.label().bind_text_from(gpu_monitor, 'vendor',
lambda x: f'({x.value})')
with ui.row():
ui.label('Usage:')
ui.label().bind_text_from(gpu_monitor, 'usage',
lambda x: f'{x:.1f}%')
ui.label('Temp:')
ui.label().bind_text_from(gpu_monitor, 'temperature',
lambda x: f'{x:.1f}°C')
with ui.row():
ui.label('Memory:')
ui.label().bind_text_from(gpu_monitor, 'memory_percent',
lambda x: f'{x:.1f}%')
ui.label().bind_text_from(gpu_monitor, 'memory_used',
lambda x: f'({(x / 1024.0):.2f} GB / {(gpu_monitor.memory_total / 1024.0):.2f} GB)')
else:
with ui.card().classes('w-full mt-4'):
ui.label('No GPU detected').classes('text-h4')
if __name__ in {"__main__", "__mp_main__"}:
ui.run(port=8081, title='System Monitor Example')

View File

@@ -6,6 +6,7 @@ from niceguiasyncelement import AsyncColumn
from components.ollama_downloader import OllamaDownloaderComponent from components.ollama_downloader import OllamaDownloaderComponent
from components.ollama_model_creation import OllamaModelCreationComponent from components.ollama_model_creation import OllamaModelCreationComponent
from components.ollama_quick_test import ModelQuickTestComponent from components.ollama_quick_test import ModelQuickTestComponent
from components.model_info import ModelInfoComponent
class OllamaManagerPage(AsyncColumn): class OllamaManagerPage(AsyncColumn):
@@ -151,7 +152,7 @@ class OllamaManagerPage(AsyncColumn):
with ui.row().classes('gap-2'): 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='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='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='info', on_click=lambda m=model['name']: self._model_information_dialog(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') 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): async def _print_model_info(self, model_name):
@@ -160,3 +161,11 @@ class OllamaManagerPage(AsyncColumn):
print(key) print(key)
print(result['modelfile']) print(result['modelfile'])
async def _model_information_dialog(self, model_name):
model_info = await ollama.model_info(model_name)
with ui.dialog().classes('max-w-none') as dialog:
await ModelInfoComponent.create(model_info)
await dialog

View File

@@ -25,9 +25,9 @@ class ExampleTool(BaseTool):
def routes(self) -> Dict[str, Callable[[], Awaitable]]: def routes(self) -> Dict[str, Callable[[], Awaitable]]:
"""Define the routes for this tool""" """Define the routes for this tool"""
return { return {
'': lambda: MainPage().create(self), '': lambda: MainPage.create(self),
'/settings': lambda: SettingsPage().create(self), '/settings': lambda: SettingsPage.create(self),
'/history': lambda: HistoryPage().create(self), '/history': lambda: HistoryPage.create(self),
} }