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),
}