From 2143074fded9e015f7eb5f21822634696ed1c8b3 Mon Sep 17 00:00:00 2001 From: Alexander Thiess Date: Thu, 18 Sep 2025 23:55:47 +0200 Subject: [PATCH] updated readme. model info added as dialog --- README.md | 2 +- docs/TOOL_CREATION.md | 10 +-- src/components/model_info.py | 155 +++++++++++++++++++++++++++++++++ src/monitor_example.py | 82 ----------------- src/pages/ollama_manager.py | 11 ++- src/tools/example_tool/tool.py | 6 +- 6 files changed, 174 insertions(+), 92 deletions(-) create mode 100644 src/components/model_info.py delete mode 100644 src/monitor_example.py diff --git a/README.md b/README.md index 74b87c9..eef6c5d 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ class MyTool(BaseTool): @property def routes(self): - return {'': lambda: MainPage().create(self)} + return {'': lambda: MainPage.create(self)} class MainPage(BasePage): async def content(self): diff --git a/docs/TOOL_CREATION.md b/docs/TOOL_CREATION.md index 7b0d296..6d07026 100644 --- a/docs/TOOL_CREATION.md +++ b/docs/TOOL_CREATION.md @@ -49,7 +49,7 @@ class MyTool(BaseTool): @property def routes(self) -> Dict[str, Callable[[], Awaitable]]: return { - '': lambda: MainPage().create(self), + '': lambda: MainPage.create(self), } class MainPage(BasePage): @@ -116,10 +116,10 @@ Tools can have multiple pages using sub-routes: @property def routes(self): return { - '': lambda: MainPage().create(self), # /my-tool - '/settings': lambda: SettingsPage().create(self), # /my-tool/settings - '/results': lambda: ResultsPage().create(self), # /my-tool/results - '/history': lambda: HistoryPage().create(self), # /my-tool/history + '': lambda: MainPage.create(self), # /my-tool + '/settings': lambda: SettingsPage.create(self), # /my-tool/settings + '/results': lambda: ResultsPage.create(self), # /my-tool/results + '/history': lambda: HistoryPage.create(self), # /my-tool/history } ``` diff --git a/src/components/model_info.py b/src/components/model_info.py new file mode 100644 index 0000000..1f1c815 --- /dev/null +++ b/src/components/model_info.py @@ -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'
{model_info["template"]}
').classes('w-full') + + if 'system' in model_info: + ui.label('System Prompt:').classes('text-sm font-bold text-white mb-2') + ui.html(f'
{model_info["system"]}
').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''' +
{model_info["modelfile"]}
+ ''').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''' +
{pretty_text}
+ ''').classes('w-full') diff --git a/src/monitor_example.py b/src/monitor_example.py deleted file mode 100644 index ecd1a65..0000000 --- a/src/monitor_example.py +++ /dev/null @@ -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') diff --git a/src/pages/ollama_manager.py b/src/pages/ollama_manager.py index 0582144..9af6213 100644 --- a/src/pages/ollama_manager.py +++ b/src/pages/ollama_manager.py @@ -6,6 +6,7 @@ from niceguiasyncelement import AsyncColumn from components.ollama_downloader import OllamaDownloaderComponent from components.ollama_model_creation import OllamaModelCreationComponent from components.ollama_quick_test import ModelQuickTestComponent +from components.model_info import ModelInfoComponent class OllamaManagerPage(AsyncColumn): @@ -151,7 +152,7 @@ class OllamaManagerPage(AsyncColumn): 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='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') async def _print_model_info(self, model_name): @@ -160,3 +161,11 @@ class OllamaManagerPage(AsyncColumn): print(key) 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 diff --git a/src/tools/example_tool/tool.py b/src/tools/example_tool/tool.py index cea4c0d..652a331 100644 --- a/src/tools/example_tool/tool.py +++ b/src/tools/example_tool/tool.py @@ -25,9 +25,9 @@ class ExampleTool(BaseTool): 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), + '': lambda: MainPage.create(self), + '/settings': lambda: SettingsPage.create(self), + '/history': lambda: HistoryPage.create(self), }