updated readme.
model info added as dialog
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
155
src/components/model_info.py
Normal file
155
src/components/model_info.py
Normal 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')
|
||||||
@@ -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')
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user