Feature: Add Settings page with centralized AI model configuration

- Add Settings page for AI model selection with load status indicators
- Add ModelWarmupService to preload configured model on app startup
- Consolidate AI model config to single AI:ReceiptParsingModel setting
- Simplify ViewReceipt and AICategorizePreview to use Settings model
- Improve AI categorization confidence prompt for varied scores

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 18:06:02 -05:00
parent 29d26b4771
commit c5fad34658
9 changed files with 306 additions and 139 deletions

View File

@@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
using MoneyMap.Services;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MoneyMap.Pages
{
@@ -14,31 +12,24 @@ namespace MoneyMap.Pages
private readonly MoneyMapContext _db;
private readonly IReceiptManager _receiptManager;
private readonly IEnumerable<IReceiptParser> _parsers;
private readonly LlamaCppVisionClient _llamaClient;
private readonly IConfiguration _config;
private readonly IWebHostEnvironment _env;
public ViewReceiptModel(
MoneyMapContext db,
IReceiptManager receiptManager,
IEnumerable<IReceiptParser> parsers,
LlamaCppVisionClient llamaClient,
IConfiguration config,
IWebHostEnvironment env)
IConfiguration config)
{
_db = db;
_receiptManager = receiptManager;
_parsers = parsers;
_llamaClient = llamaClient;
_config = config;
_env = env;
}
public Receipt? Receipt { get; set; }
public List<ReceiptLineItem> LineItems { get; set; } = new();
public List<ReceiptParseLog> ParseLogs { get; set; } = new();
public List<ParserOption> AvailableParsers { get; set; } = new();
public List<LlamaCppModel> AvailableModels { get; set; } = new();
public string ReceiptUrl { get; set; } = "";
public string SelectedModel => _config["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
@@ -72,9 +63,6 @@ namespace MoneyMap.Pages
FullName = p.GetType().Name
}).ToList();
// Get available LlamaCpp models
AvailableModels = await _llamaClient.GetAvailableModelsAsync();
return Page();
}
@@ -100,7 +88,7 @@ namespace MoneyMap.Pages
return File(fileBytes, receipt.ContentType);
}
public async Task<IActionResult> OnPostParseAsync(long id, string parser, string? model = null)
public async Task<IActionResult> OnPostParseAsync(long id, string parser)
{
var selectedParser = _parsers.FirstOrDefault(p => p.GetType().Name == parser);
@@ -110,13 +98,8 @@ namespace MoneyMap.Pages
return RedirectToPage(new { id });
}
// Save selected model to config if it changed
if (!string.IsNullOrEmpty(model) && model != SelectedModel)
{
await SaveSelectedModelAsync(model);
}
var result = await selectedParser.ParseReceiptAsync(id, model);
// Use the configured model from settings
var result = await selectedParser.ParseReceiptAsync(id, SelectedModel);
if (result.IsSuccess)
{
@@ -130,33 +113,6 @@ namespace MoneyMap.Pages
return RedirectToPage(new { id });
}
private async Task SaveSelectedModelAsync(string model)
{
try
{
var appSettingsPath = Path.Combine(_env.ContentRootPath, "appsettings.json");
var json = await System.IO.File.ReadAllTextAsync(appSettingsPath);
var jsonNode = JsonNode.Parse(json);
if (jsonNode == null) return;
// Ensure AI section exists
if (jsonNode["AI"] == null)
{
jsonNode["AI"] = new JsonObject();
}
jsonNode["AI"]!["ReceiptParsingModel"] = model;
var options = new JsonSerializerOptions { WriteIndented = true };
await System.IO.File.WriteAllTextAsync(appSettingsPath, jsonNode.ToJsonString(options));
}
catch
{
// Silently fail - not critical if we can't save the preference
}
}
public class ParserOption
{
public string Name { get; set; } = "";