Skip to content

Commit da63125

Browse files
committed
Refactor settings, utils and startup logic
Several refactors and behavior fixes across the app: - NavBar: call SettingsState.RequestSettings directly from the settings button, simplify theme toggle and reload helpers, remove unused flag/methods. - Home: prevent adding image inputs when the selected model doesn't support vision and display an error. - Settings page: build backend options with Prepend for simpler ordering. - Program.cs: validate Self backend model at startup and exit if unsupported; adjust AddMaIN registration flow based on NeedsConfiguration and BackendType. - SettingsService: convert to constructor-injected style, consolidate load/save into generic dict helpers, and add typed methods for saving/getting API keys and model history. - SettingsStateService: shorten comment describing the event bus. - Utils: unify capability checks with a generic GetCapability<T>, make Reason mutually exclusive with ImageGen, and streamline environment variable handling when setting backend API keys. These changes simplify codepaths, centralize settings persistence logic, and enforce model capability checks earlier and at the UI level.
1 parent 6c4e880 commit da63125

7 files changed

Lines changed: 69 additions & 165 deletions

File tree

src/MaIN.InferPage/Components/Layout/NavBar.razor

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<FluentButton Style="background-color: transparent;"
5555
BackgroundColor="rgba(0, 0, 0, 0)"
5656
Appearance="Appearance.Lightweight"
57-
OnClick="@OpenSettings" IconStart="@(new Icons.Regular.Size24.Settings().WithColor(AccentColor))">
57+
OnClick="@(() => SettingsState.RequestSettings())" IconStart="@(new Icons.Regular.Size24.Settings().WithColor(AccentColor))">
5858
</FluentButton>
5959
<FluentButton Style="padding: 10px; background-color: transparent;"
6060
BackgroundColor="rgba(0, 0, 0, 0)"
@@ -72,7 +72,6 @@
7272
@code {
7373
private DesignThemeModes Mode { get; set; }
7474
private string AccentColor => Mode == DesignThemeModes.Dark ? "#00ffcc" : "#00cca3";
75-
private bool _isChangingTheme = false;
7675

7776
protected override async Task OnAfterRenderAsync(bool firstRender)
7877
{
@@ -91,22 +90,10 @@
9190
}
9291

9392
private void SetTheme()
94-
{
95-
if (_isChangingTheme) return;
96-
_isChangingTheme = true;
97-
Mode = Mode == DesignThemeModes.Dark ? DesignThemeModes.Light : DesignThemeModes.Dark;
98-
_isChangingTheme = false;
99-
}
93+
=> Mode = Mode == DesignThemeModes.Dark ? DesignThemeModes.Light : DesignThemeModes.Dark;
10094

101-
private void Reload(MouseEventArgs obj)
102-
{
103-
_navigationManager.Refresh(true);
104-
}
105-
106-
private void OpenSettings()
107-
{
108-
SettingsState.RequestSettings();
109-
}
95+
private void Reload()
96+
=> _navigationManager.Refresh(true);
11097

11198
private string GetBackendColor()
11299
{

src/MaIN.InferPage/Components/Pages/Home.razor

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,11 @@
460460

461461
if (ImageExtensions.Contains(extension))
462462
{
463+
if (!Utils.Vision)
464+
{
465+
_errorMessage = "This model doesn't support image input.";
466+
continue;
467+
}
463468
var base64 = Convert.ToBase64String(ms.ToArray());
464469
ms.Position = 0;
465470
_selectedImages.Add((fileInfo, base64));
@@ -506,6 +511,11 @@
506511

507512
if (ImageExtensions.Contains(extension.ToLowerInvariant()))
508513
{
514+
if (!Utils.Vision)
515+
{
516+
_errorMessage = "This model doesn't support image input.";
517+
return;
518+
}
509519
_selectedImages.Add((fileInfo, base64Data));
510520
}
511521
else

src/MaIN.InferPage/Components/Pages/Settings.razor

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,7 @@
202202

203203
protected override async Task OnInitializedAsync()
204204
{
205-
var localOption = new BackendOption(0, "Local", BackendType.Self, false);
206-
207-
var backendOptions = new List<BackendOption>
205+
_backendOptions = new List<BackendOption>
208206
{
209207
new(1, "OpenAI", BackendType.OpenAi, true),
210208
new(2, "Gemini", BackendType.Gemini, true),
@@ -214,11 +212,9 @@
214212
new(6, "xAI", BackendType.Xai, true),
215213
new(7, "Ollama (Local)", BackendType.Ollama, false),
216214
new(8, "Ollama (Cloud)", BackendType.Ollama, true)
217-
218-
}.OrderBy(x => x.DisplayName).ToList();
219-
220-
backendOptions.Insert(0, localOption);
221-
_backendOptions = backendOptions;
215+
}.OrderBy(x => x.DisplayName)
216+
.Prepend(new BackendOption(0, "Local", BackendType.Self, false))
217+
.ToList();
222218

223219
await base.OnInitializedAsync();
224220
}

src/MaIN.InferPage/Program.cs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,28 +100,19 @@
100100
return;
101101
}
102102

103-
if (Utils.NeedsConfiguration)
103+
// For Self backend CLI mode, validate model before registering
104+
if (!Utils.NeedsConfiguration && Utils.BackendType == BackendType.Self
105+
&& Utils.Path == null && !ModelRegistry.Exists(Utils.Model!))
104106
{
105-
// Register with defaults — will be reconfigured at runtime via Settings UI
106-
builder.Services.AddMaIN(builder.Configuration);
107-
}
108-
else if (Utils.BackendType != BackendType.Self)
109-
{
110-
builder.Services.AddMaIN(builder.Configuration, settings =>
111-
{
112-
settings.BackendType = Utils.BackendType;
113-
});
107+
Console.WriteLine($"Model: {Utils.Model} is not supported");
108+
Environment.Exit(0);
114109
}
115-
else
116-
{
117-
if (Utils.Path == null && !ModelRegistry.Exists(Utils.Model!))
118-
{
119-
Console.WriteLine($"Model: {Utils.Model} is not supported");
120-
Environment.Exit(0);
121-
}
122110

111+
if (!Utils.NeedsConfiguration && Utils.BackendType != BackendType.Self)
112+
builder.Services.AddMaIN(builder.Configuration, s => s.BackendType = Utils.BackendType);
113+
else
114+
// NeedsConfiguration or Self backend: register with defaults
123115
builder.Services.AddMaIN(builder.Configuration);
124-
}
125116

126117
var app = builder.Build();
127118

src/MaIN.InferPage/Services/SettingsService.cs

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,37 @@
22

33
namespace MaIN.InferPage.Services;
44

5-
public class SettingsService
5+
public class SettingsService(IJSRuntime js)
66
{
77
private const string SettingsKey = "inferpage-settings";
88
private const string ApiKeysKey = "inferpage-apikeys";
99
private const string ModelHistoryKey = "inferpage-model-history";
1010

11-
private readonly IJSRuntime _js;
12-
13-
public SettingsService(IJSRuntime js)
14-
{
15-
_js = js;
16-
}
17-
1811
public async Task<InferPageSettings?> LoadSettingsAsync()
19-
{
20-
return await _js.InvokeAsync<InferPageSettings?>("settingsManager.load", SettingsKey);
21-
}
12+
=> await js.InvokeAsync<InferPageSettings?>("settingsManager.load", SettingsKey);
2213

2314
public async Task SaveSettingsAsync(InferPageSettings settings)
24-
{
25-
await _js.InvokeVoidAsync("settingsManager.save", SettingsKey, settings);
26-
}
15+
=> await js.InvokeVoidAsync("settingsManager.save", SettingsKey, settings);
2716

2817
public async Task<bool> HasSettingsAsync()
29-
{
30-
return await _js.InvokeAsync<bool>("settingsManager.exists", SettingsKey);
31-
}
18+
=> await js.InvokeAsync<bool>("settingsManager.exists", SettingsKey);
3219

33-
public async Task<Dictionary<string, string>?> LoadApiKeysAsync()
34-
{
35-
return await _js.InvokeAsync<Dictionary<string, string>?>("settingsManager.load", ApiKeysKey);
36-
}
20+
public Task SaveApiKeyAsync(string backend, string key) => SetInDictAsync(ApiKeysKey, backend, key);
21+
public Task<string?> GetApiKeyForBackendAsync(string backend) => GetFromDictAsync(ApiKeysKey, backend);
3722

38-
public async Task SaveApiKeyAsync(string backendName, string key)
39-
{
40-
var keys = await LoadApiKeysAsync() ?? new Dictionary<string, string>();
41-
keys[backendName] = key;
42-
await _js.InvokeVoidAsync("settingsManager.save", ApiKeysKey, keys);
43-
}
44-
45-
public async Task<string?> GetApiKeyForBackendAsync(string backendName)
46-
{
47-
var keys = await LoadApiKeysAsync();
48-
return keys?.GetValueOrDefault(backendName);
49-
}
23+
public Task SaveModelForBackendAsync(string backend, string model) => SetInDictAsync(ModelHistoryKey, backend, model);
24+
public Task<string?> GetLastModelForBackendAsync(string backend) => GetFromDictAsync(ModelHistoryKey, backend);
5025

51-
public async Task<Dictionary<string, string>?> LoadModelHistoryAsync()
26+
private async Task SetInDictAsync(string storageKey, string key, string value)
5227
{
53-
return await _js.InvokeAsync<Dictionary<string, string>?>("settingsManager.load", ModelHistoryKey);
28+
var dict = await LoadDictAsync(storageKey);
29+
dict[key] = value;
30+
await js.InvokeVoidAsync("settingsManager.save", storageKey, dict);
5431
}
5532

56-
public async Task SaveModelForBackendAsync(string backendName, string model)
57-
{
58-
var history = await LoadModelHistoryAsync() ?? new Dictionary<string, string>();
59-
history[backendName] = model;
60-
await _js.InvokeVoidAsync("settingsManager.save", ModelHistoryKey, history);
61-
}
33+
private async Task<string?> GetFromDictAsync(string storageKey, string key)
34+
=> (await LoadDictAsync(storageKey)).GetValueOrDefault(key);
6235

63-
public async Task<string?> GetLastModelForBackendAsync(string backendName)
64-
{
65-
var history = await LoadModelHistoryAsync();
66-
return history?.GetValueOrDefault(backendName);
67-
}
68-
}
36+
private async Task<Dictionary<string, string>> LoadDictAsync(string storageKey)
37+
=> await js.InvokeAsync<Dictionary<string, string>?>("settingsManager.load", storageKey) ?? new();
38+
}

src/MaIN.InferPage/Services/SettingsStateService.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
namespace MaIN.InferPage.Services;
22

3-
/// <summary>
4-
/// Scoped service for cross-component settings event communication.
5-
/// NavBar (interactive) fires events, Home (interactive) subscribes.
6-
/// </summary>
3+
/// <summary>Event bus for NavBar ↔ Home sibling communication.</summary>
74
public class SettingsStateService
85
{
96
public event Action? OnSettingsRequested;

src/MaIN.InferPage/Utils.cs

Lines changed: 24 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,26 @@ public static class Utils
1111
public static bool HasApiKey { get; set; }
1212
public static string? Path { get; set; }
1313
public static bool IsLocal => BackendType == BackendType.Self || (BackendType == BackendType.Ollama && !HasApiKey);
14-
public static string? Model = "gemma3-4b";
14+
public static string? Model;
15+
1516
public static bool NeedsConfiguration { get; set; }
1617

1718
// Manual capability overrides for unregistered models (set from Settings UI)
1819
public static bool? ManualVision { get; set; }
1920
public static bool? ManualReasoning { get; set; }
2021
public static bool? ManualImageGen { get; set; }
2122

22-
public static bool Reason
23-
{
24-
get
25-
{
26-
if (string.IsNullOrEmpty(Model)) return false;
27-
if (ModelRegistry.TryGetById(Model, out var m))
28-
return m is IReasoningModel && !ImageGen; // reasoning and image gen are mutually exclusive
29-
if (ManualReasoning.HasValue) return ManualReasoning.Value;
30-
return false;
31-
}
32-
}
33-
34-
public static bool ImageGen
35-
{
36-
get
37-
{
38-
if (string.IsNullOrEmpty(Model)) return false;
39-
if (ModelRegistry.TryGetById(Model, out var m))
40-
return m is IImageGenerationModel;
41-
if (ManualImageGen.HasValue) return ManualImageGen.Value;
42-
return ImageGenerationModels.Contains(Model); // fallback for unregistered models (e.g. FLUX via separate server)
43-
}
44-
}
23+
// registry → manual override → fallback set (null = no fallback)
24+
private static bool GetCapability<T>(bool? manual, HashSet<string>? fallback = null)
25+
where T : class =>
26+
!string.IsNullOrEmpty(Model) && (
27+
ModelRegistry.TryGetById(Model, out var m) ? m is T :
28+
manual.HasValue ? manual.Value :
29+
fallback?.Contains(Model) ?? false);
4530

46-
public static bool Vision
47-
{
48-
get
49-
{
50-
if (string.IsNullOrEmpty(Model)) return false;
51-
if (ModelRegistry.TryGetById(Model, out var m))
52-
return m is IVisionModel;
53-
if (ManualVision.HasValue) return ManualVision.Value;
54-
return VisionModels.Contains(Model); // fallback for unregistered models
55-
}
56-
}
31+
public static bool ImageGen => GetCapability<IImageGenerationModel>(ManualImageGen, ImageGenerationModels);
32+
public static bool Vision => GetCapability<IVisionModel>(ManualVision, VisionModels);
33+
public static bool Reason => GetCapability<IReasoningModel>(ManualReasoning);
5734

5835
public static bool IsKnownVisionModel(string model) => VisionModels.Contains(model);
5936
public static bool IsKnownImageGenModel(string model) => ImageGenerationModels.Contains(model);
@@ -95,44 +72,20 @@ public static void ApplySettings(
9572

9673
mainSettings.BackendType = backendType;
9774

98-
if (!string.IsNullOrEmpty(apiKey))
99-
{
100-
var entry = LLMApiRegistry.GetEntry(backendType);
101-
if (entry != null)
102-
{
103-
Environment.SetEnvironmentVariable(entry.ApiKeyEnvName, apiKey);
104-
}
75+
// null clears env var and key; handles both "set new key" and "clear stale key" cases
76+
var entry = LLMApiRegistry.GetEntry(backendType);
77+
if (entry != null)
78+
Environment.SetEnvironmentVariable(entry.ApiKeyEnvName, apiKey);
10579

106-
switch (backendType)
107-
{
108-
case BackendType.OpenAi: mainSettings.OpenAiKey = apiKey; break;
109-
case BackendType.Gemini: mainSettings.GeminiKey = apiKey; break;
110-
case BackendType.DeepSeek: mainSettings.DeepSeekKey = apiKey; break;
111-
case BackendType.Anthropic: mainSettings.AnthropicKey = apiKey; break;
112-
case BackendType.GroqCloud: mainSettings.GroqCloudKey = apiKey; break;
113-
case BackendType.Ollama: mainSettings.OllamaKey = apiKey; break;
114-
case BackendType.Xai: mainSettings.XaiKey = apiKey; break;
115-
}
116-
}
117-
else
80+
switch (backendType)
11881
{
119-
// Clear stale key for this backend (e.g. switching Ollama Cloud → Local)
120-
var entry = LLMApiRegistry.GetEntry(backendType);
121-
if (entry != null)
122-
{
123-
Environment.SetEnvironmentVariable(entry.ApiKeyEnvName, null);
124-
}
125-
126-
switch (backendType)
127-
{
128-
case BackendType.OpenAi: mainSettings.OpenAiKey = null; break;
129-
case BackendType.Gemini: mainSettings.GeminiKey = null; break;
130-
case BackendType.DeepSeek: mainSettings.DeepSeekKey = null; break;
131-
case BackendType.Anthropic: mainSettings.AnthropicKey = null; break;
132-
case BackendType.GroqCloud: mainSettings.GroqCloudKey = null; break;
133-
case BackendType.Ollama: mainSettings.OllamaKey = null; break;
134-
case BackendType.Xai: mainSettings.XaiKey = null; break;
135-
}
82+
case BackendType.OpenAi: mainSettings.OpenAiKey = apiKey; break;
83+
case BackendType.Gemini: mainSettings.GeminiKey = apiKey; break;
84+
case BackendType.DeepSeek: mainSettings.DeepSeekKey = apiKey; break;
85+
case BackendType.Anthropic: mainSettings.AnthropicKey = apiKey; break;
86+
case BackendType.GroqCloud: mainSettings.GroqCloudKey = apiKey; break;
87+
case BackendType.Ollama: mainSettings.OllamaKey = apiKey; break;
88+
case BackendType.Xai: mainSettings.XaiKey = apiKey; break;
13689
}
13790
}
13891

0 commit comments

Comments
 (0)