2026-05-14 12:31:35 +02:00
|
|
|
@page "/models"
|
2026-05-21 11:04:42 +02:00
|
|
|
@attribute [Authorize]
|
2026-05-14 12:31:35 +02:00
|
|
|
@inject IOllamaService OllamaService
|
|
|
|
|
@inject NavigationManager Navigation
|
2026-05-25 06:38:16 +02:00
|
|
|
@inject ILocalizationService Loc
|
2026-05-16 16:13:35 +02:00
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
<PageTitle>@Loc["page.title.models"]</PageTitle>
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
2026-05-25 06:38:16 +02:00
|
|
|
<h3>@Loc["models.heading"]</h3>
|
|
|
|
|
<button @onclick="RefreshModels" disabled="@isLoading">@Loc["models.button.refresh"]</button>
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (isLoading)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<p>@Loc["models.status.loading"]</p>
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
else if (!string.IsNullOrEmpty(errorMessage))
|
|
|
|
|
{
|
|
|
|
|
<div style="color: red; margin-bottom: 12px;">@errorMessage</div>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<div class="model-list">
|
|
|
|
|
@foreach (var model in localModels)
|
|
|
|
|
{
|
|
|
|
|
<div class="model-card">
|
2026-05-16 16:13:35 +02:00
|
|
|
<div class="model-name">@model.Name</div>
|
2026-05-14 12:31:35 +02:00
|
|
|
<div class="model-details">
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["models.label.size"] @FormatSize(model.Size) |
|
|
|
|
|
@Loc["models.label.modified"] @model.ModifiedAt?.ToString("g")
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<div style="margin-top: 24px; border-top: 1px solid var(--border-color); padding-top: 16px;">
|
2026-05-25 06:38:16 +02:00
|
|
|
<h4 style="margin-bottom: 8px;">@Loc["models.pull.heading"]</h4>
|
2026-05-14 12:31:35 +02:00
|
|
|
<div style="display: flex; gap: 8px; align-items: flex-end;">
|
|
|
|
|
<div class="form-group" style="flex: 1; margin-bottom: 0;">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label for="pullModelName">@Loc["models.pull.label.name"]</label>
|
|
|
|
|
<input id="pullModelName" @bind="modelToPull" placeholder="@Loc["models.pull.placeholder"]" />
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
<button @onclick="PullModel" disabled="@(isPulling || string.IsNullOrWhiteSpace(modelToPull))">
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["models.pull.button.download"]
|
2026-05-14 12:31:35 +02:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
@if (isPulling)
|
|
|
|
|
{
|
|
|
|
|
<div style="margin-top: 8px;">
|
|
|
|
|
<div class="progress-bar">
|
|
|
|
|
<div class="progress-fill" style="width: @pullPercentDisplay"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="font-size: 12px; margin-top: 4px;">@pullStatus (@pullPercentDisplay)</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (pullResult != null)
|
|
|
|
|
{
|
|
|
|
|
<div style="margin-top: 12px; padding: 8px; background: var(--card-bg); border: 1px solid var(--accent-color); font-size: 13px;">
|
|
|
|
|
@pullResult
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@code {
|
2026-05-16 16:13:35 +02:00
|
|
|
private List<LLM_Model> localModels = new();
|
2026-05-14 12:31:35 +02:00
|
|
|
private bool isLoading;
|
|
|
|
|
private string? errorMessage;
|
|
|
|
|
private string modelToPull = string.Empty;
|
|
|
|
|
private bool isPulling;
|
|
|
|
|
private double pullProgress;
|
|
|
|
|
private string pullStatus = string.Empty;
|
|
|
|
|
private string? pullResult;
|
|
|
|
|
private string pullPercentDisplay => $"{pullProgress:F0}%";
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
await RefreshModels();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RefreshModels()
|
|
|
|
|
{
|
|
|
|
|
isLoading = true;
|
|
|
|
|
errorMessage = null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var models = await OllamaService.GetLocalModelsAsync();
|
|
|
|
|
localModels = models.ToList();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
errorMessage = $"{Loc["models.error.loading"]} {ex.Message}";
|
2026-05-14 12:31:35 +02:00
|
|
|
localModels = new();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task PullModel()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(modelToPull)) return;
|
|
|
|
|
|
|
|
|
|
isPulling = true;
|
|
|
|
|
pullProgress = 0;
|
2026-05-25 06:38:16 +02:00
|
|
|
pullStatus = Loc["models.pull.status.starting"];
|
2026-05-14 12:31:35 +02:00
|
|
|
pullResult = null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-16 16:13:35 +02:00
|
|
|
var progress = new Progress<string>(status =>
|
2026-05-14 12:31:35 +02:00
|
|
|
{
|
2026-05-16 16:13:35 +02:00
|
|
|
pullStatus = status;
|
2026-05-14 12:31:35 +02:00
|
|
|
InvokeAsync(StateHasChanged);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await OllamaService.PullModelAsync(modelToPull, progress);
|
2026-05-25 06:38:16 +02:00
|
|
|
pullResult = $"{Loc["models.pull.status.model"]} '{modelToPull}' {Loc["models.pull.status.success"]}";
|
2026-05-14 12:31:35 +02:00
|
|
|
modelToPull = string.Empty;
|
|
|
|
|
await RefreshModels();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
pullResult = $"{Loc["common.error"]} {ex.Message}";
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
isPulling = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string FormatSize(long? bytes)
|
|
|
|
|
{
|
2026-05-20 07:27:41 +02:00
|
|
|
if (!bytes.HasValue) return "---";
|
2026-05-14 12:31:35 +02:00
|
|
|
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
|
|
|
|
|
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
|
|
|
|
|
return $"{bytes / (1024.0 * 1024 * 1024):F1} GB";
|
|
|
|
|
}
|
|
|
|
|
}
|