diff --git a/src/components/ollama_downloader.py b/src/components/ollama_downloader.py
index 75fd60c..d293899 100644
--- a/src/components/ollama_downloader.py
+++ b/src/components/ollama_downloader.py
@@ -12,6 +12,7 @@ class OllamaDownloaderComponent(AsyncCard):
self.download_status = ''
async def build(self) -> None:
+ self.classes('w-full')
with self:
with ui.column().classes('w-full gap-4'):
ui.label('Model Downloader').classes('text-xl font-bold')
@@ -21,8 +22,9 @@ class OllamaDownloaderComponent(AsyncCard):
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.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)
diff --git a/src/components/ollama_model_creation.py b/src/components/ollama_model_creation.py
index b37d827..b5c7c0c 100644
--- a/src/components/ollama_model_creation.py
+++ b/src/components/ollama_model_creation.py
@@ -4,12 +4,6 @@ from pathlib import Path
from utils import ollama
from typing import Optional
-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()
@@ -74,33 +68,59 @@ class OllamaModelCreationComponent(AsyncCard):
self.use_seed = False
self.use_stop = False
- async def build(self) -> None:
+ async def build(self, existing_model_name: Optional[str] = None) -> None:
self.classes('w-full')
+
+ # Load existing model data if provided
+ if existing_model_name:
+ await self.load_existing_model(existing_model_name)
+
with self:
with ui.column().classes('w-full gap-4'):
- ui.label('Create Model').classes('text-xl font-bold')
+ title = 'Edit Model' if existing_model_name else 'Create Model'
+ ui.label(title).classes('text-xl font-bold')
# Basic fields
- ui.input('Model Name', value='my-custom-model:latest').props('outlined dense').classes('w-full').bind_value(self, 'model_name')
- ui.input('Base Model', value='llama3.2:3b').props('outlined dense').classes('w-full').bind_value(self, 'model_from')
+ model_name_default = existing_model_name if existing_model_name else 'my-custom-model:latest'
+ base_model_default = '' if existing_model_name else 'llama3.2:3b'
+
+ ui.input('Model Name', value=model_name_default).props('outlined dense').classes('w-full').bind_value(self, 'model_name')
+ ui.input('Base Model', value=base_model_default).props('outlined dense').classes('w-full').bind_value(self, 'model_from')
# System message field (commonly used)
ui.textarea('System Message', placeholder='You are a helpful assistant...').classes('w-full').props('autogrow outlined').bind_value(self, 'system_message')
- # Parameters section
- ui.label('Parameters').classes('text-md font-medium mt-2 mb-3')
+ # Common Parameters section
+ ui.label('Common Parameters').classes('text-md font-medium mt-2 mb-3')
+
+ # Temperature (always visible)
+ with ui.row().classes('items-center gap-3 w-full mb-3'):
+ ui.switch().bind_value(self, 'use_temperature')
+ ui.label('Temperature').classes('min-w-fit')
+ ui.slider(min=0.0, max=2.0, step=0.1).classes('flex-1').bind_value(self, 'temperature').bind_enabled_from(self, 'use_temperature')
+ ui.label().bind_text_from(self, 'temperature', backward=lambda x: f'{x:.1f}').classes('text-xs text-gray-500 min-w-fit')
+ ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('The temperature of the model. Higher values (e.g., 1.2) make output more creative, lower values (e.g., 0.5) more focused. Default: 0.8')
+
+ # Context Length (always visible)
+ with ui.row().classes('items-center gap-3 w-full mb-3'):
+ ui.switch().bind_value(self, 'use_num_ctx')
+ ui.label('Context Length').classes('min-w-fit')
+ ui.number(value=4096, min=1, max=32768).classes('flex-1').bind_value(self, 'num_ctx').bind_enabled_from(self, 'use_num_ctx')
+ ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Size of the context window used to generate the next token. Default: 4096')
+
+ # Max Tokens (always visible)
+ with ui.row().classes('items-center gap-3 w-full mb-4'):
+ ui.switch().bind_value(self, 'use_num_predict')
+ ui.label('Max Tokens').classes('min-w-fit')
+ ui.number(value=-1, min=-1, max=4096).classes('flex-1').bind_value(self, 'num_predict').bind_enabled_from(self, 'use_num_predict')
+ ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Maximum number of tokens to predict. -1 for infinite generation. Default: -1')
+
+ # Advanced Parameters section
+ ui.label('Advanced Parameters').classes('text-md font-medium mt-2 mb-3')
# Generation Parameters
with ui.expansion('Generation', icon='tune').classes('w-full mb-2'):
with ui.column().classes('w-full gap-3 pt-2'):
- # Temperature
- with ui.row().classes('items-center gap-3 w-full'):
- ui.switch().bind_value(self, 'use_temperature')
- ui.label('Temperature').classes('min-w-fit')
- ui.slider(min=0.0, max=2.0, step=0.1).classes('flex-1').bind_value(self, 'temperature').bind_enabled_from(self, 'use_temperature')
- ui.label().bind_text_from(self, 'temperature', backward=lambda x: f'{x:.1f}').classes('text-xs text-gray-500 min-w-fit')
- ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('The temperature of the model. Higher values (e.g., 1.2) make output more creative, lower values (e.g., 0.5) more focused. Default: 0.8')
-
# Top K
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_top_k')
@@ -124,23 +144,6 @@ class OllamaModelCreationComponent(AsyncCard):
ui.label().bind_text_from(self, 'min_p', backward=lambda x: f'{x:.2f}').classes('text-xs text-gray-500 min-w-fit')
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Alternative to top_p. Minimum probability for a token relative to the most likely token. Default: 0.0')
- # Context Parameters
- with ui.expansion('Context', icon='memory').classes('w-full mb-2'):
- with ui.column().classes('w-full gap-3 pt-2'):
- # Context Length
- with ui.row().classes('items-center gap-3 w-full'):
- ui.switch().bind_value(self, 'use_num_ctx')
- ui.label('Context Length').classes('min-w-fit')
- ui.number(value=4096, min=1, max=32768).classes('flex-1').bind_value(self, 'num_ctx').bind_enabled_from(self, 'use_num_ctx')
- ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Size of the context window used to generate the next token. Default: 4096')
-
- # Max Tokens
- with ui.row().classes('items-center gap-3 w-full'):
- ui.switch().bind_value(self, 'use_num_predict')
- ui.label('Max Tokens').classes('min-w-fit')
- ui.number(value=-1, min=-1, max=4096).classes('flex-1').bind_value(self, 'num_predict').bind_enabled_from(self, 'use_num_predict')
- ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Maximum number of tokens to predict. -1 for infinite generation. Default: -1')
-
# Repetition Parameters
with ui.expansion('Repetition Control', icon='repeat').classes('w-full mb-2'):
with ui.column().classes('w-full gap-3 pt-2'):
@@ -194,7 +197,82 @@ class OllamaModelCreationComponent(AsyncCard):
ui.linear_progress(value=0, show_value=False).props('buffer=0.0 animation-speed=0').bind_value_from(self, 'download_progress')
# Create button
- self.create_btn = ui.button('Create Model', icon='add', on_click=self.create_model).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_name', backward=lambda x: bool(x) and not self.is_downloading)
+ button_text = 'Update Model' if existing_model_name else 'Create Model'
+ self.create_btn = ui.button(button_text, icon='add', on_click=self.create_model).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_name', backward=lambda x: bool(x) and not self.is_downloading)
+
+ async def load_existing_model(self, model_name):
+ """Load existing model data and populate form fields"""
+ try:
+ model_info = await ollama.model_info(model_name)
+ modelfile = model_info.get('modelfile', '')
+
+ # Parse the modelfile content to extract settings
+ for line in modelfile.split('\n'):
+ line = line.strip()
+ if not line:
+ continue
+
+ if line.startswith('FROM '):
+ self.model_from = line[5:].strip()
+ elif line.startswith('SYSTEM '):
+ # Extract system message (remove quotes)
+ system_msg = line[7:].strip()
+ if system_msg.startswith('"') and system_msg.endswith('"'):
+ system_msg = system_msg[1:-1]
+ self.system_message = system_msg
+ elif line.startswith('TEMPLATE '):
+ # Extract template (remove quotes)
+ template = line[9:].strip()
+ if template.startswith('"') and template.endswith('"'):
+ template = template[1:-1]
+ self.template = template
+ elif line.startswith('PARAMETER '):
+ # Parse parameter lines
+ param_line = line[10:].strip()
+ try:
+ key, value = param_line.split(' ', 1)
+
+ # Set parameter values and enable toggles
+ if key == 'temperature':
+ self.temperature = float(value)
+ self.use_temperature = True
+ elif key == 'top_k':
+ self.top_k = int(value)
+ self.use_top_k = True
+ elif key == 'top_p':
+ self.top_p = float(value)
+ self.use_top_p = True
+ elif key == 'min_p':
+ self.min_p = float(value)
+ self.use_min_p = True
+ elif key == 'num_ctx':
+ self.num_ctx = int(value)
+ self.use_num_ctx = True
+ elif key == 'num_predict':
+ self.num_predict = int(value)
+ self.use_num_predict = True
+ elif key == 'repeat_last_n':
+ self.repeat_last_n = int(value)
+ self.use_repeat_last_n = True
+ elif key == 'repeat_penalty':
+ self.repeat_penalty = float(value)
+ self.use_repeat_penalty = True
+ elif key == 'seed':
+ self.seed = int(value)
+ self.use_seed = True
+ elif key == 'stop':
+ # Remove quotes from stop sequence
+ stop_value = value.strip()
+ if stop_value.startswith('"') and stop_value.endswith('"'):
+ stop_value = stop_value[1:-1]
+ self.stop = stop_value
+ self.use_stop = True
+ except ValueError:
+ # Skip invalid parameter lines
+ continue
+
+ except Exception as e:
+ ui.notify(f'Error loading model info: {str(e)}', type='negative')
async def create_model(self):
# Validate required fields
diff --git a/src/pages/ollama_manager.py b/src/pages/ollama_manager.py
index 9af6213..e734d43 100644
--- a/src/pages/ollama_manager.py
+++ b/src/pages/ollama_manager.py
@@ -1,6 +1,6 @@
from nicegui import ui
from utils import ollama
-from typing import Literal, List, Dict
+from typing import Literal, List, Dict, Optional
from pprint import pprint
from niceguiasyncelement import AsyncColumn
from components.ollama_downloader import OllamaDownloaderComponent
@@ -62,7 +62,7 @@ class OllamaManagerPage(AsyncColumn):
self.models_container.refresh()
async def _download_model_dialog(self):
- with ui.dialog() as dialog:
+ with ui.dialog().classes('max-w-none') as dialog:
await OllamaDownloaderComponent.create()
await dialog
self.models_container.refresh()
@@ -96,14 +96,26 @@ class OllamaManagerPage(AsyncColumn):
# self.quick_test_select.set_options(select_options)
for model in self.models:
- self._create_model_item(model)
+ await self._create_model_item(model)
- def _create_model_item(self, model: Dict):
+ async def _create_model_item(self, model: Dict):
+ model_info = await ollama.model_info(model['name'])
+ base_model: Optional[Literal['ollama', 'huggingface']] = None
+ if len(model_info['details']['parent_model']) == 0:
+ if model['name'].startswith('hf.co/'):
+ base_model = 'huggingface'
+ else:
+ base_model = 'ollama'
with ui.card().classes('w-full'):
with ui.row().classes('w-full items-center'):
with ui.column().classes('flex-grow gap-1'):
# Model name
- ui.label(model['name']).classes('font-bold text-h6')
+ with ui.row().classes('items-center'):
+ ui.label(model['name']).classes('font-bold text-h6')
+ if base_model == 'ollama':
+ ui.html(f'
')
+ if base_model == 'huggingface':
+ ui.html(f'
')
# Details row with chips
with ui.row().classes('gap-2 flex-wrap'):
@@ -151,10 +163,16 @@ 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='edit', on_click=lambda m=model['name']: self._model_edit_model_dialog(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 _model_edit_model_dialog(self, model_name):
+ with ui.dialog() as dialog:
+ await OllamaModelCreationComponent.create(model_name)
+ await dialog
+ self.models_container.refresh()
+
async def _print_model_info(self, model_name):
result = await ollama.model_info(model_name)
for key, value in result.items():