Fix: ViewReceipt parse button now uses parse queue instead of direct parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 19:26:40 -05:00
parent e6512f9b7f
commit 324ab2c627
3 changed files with 33 additions and 55 deletions

View File

@@ -156,28 +156,20 @@
<strong>Parse Receipt</strong> <strong>Parse Receipt</strong>
</div> </div>
<div class="card-body"> <div class="card-body">
@if (Model.AvailableParsers.Any()) <form method="post" asp-page-handler="Parse" asp-route-id="@Model.Receipt.Id">
{ <p class="text-muted small mb-2">
<form method="post" asp-page-handler="Parse" asp-route-id="@Model.Receipt.Id"> Using: <strong>@Model.SelectedModel</strong>
<input type="hidden" name="parser" value="@Model.AvailableParsers.First().FullName" /> <a href="/Settings" class="ms-2 small">Change</a>
<p class="text-muted small mb-2"> </p>
Using: <strong>@Model.SelectedModel</strong> <div class="mb-2">
<a href="/Settings" class="ms-2 small">Change</a> <label for="ParsingNotes" class="form-label small text-muted mb-1">Notes for AI</label>
</p> <textarea asp-for="ParsingNotes" class="form-control form-control-sm" rows="3"
<div class="mb-2"> placeholder="Optional hints for parsing (e.g., 'This is a restaurant receipt', 'Ignore the voided items')"></textarea>
<label for="ParsingNotes" class="form-label small text-muted mb-1">Notes for AI</label> </div>
<textarea asp-for="ParsingNotes" class="form-control form-control-sm" rows="3" <button type="submit" class="btn btn-primary btn-sm w-100">
placeholder="Optional hints for parsing (e.g., 'This is a restaurant receipt', 'Ignore the voided items')"></textarea> Parse Receipt
</div> </button>
<button type="submit" class="btn btn-primary btn-sm w-100"> </form>
Parse Receipt
</button>
</form>
}
else
{
<p class="text-muted small mb-0">No parsers available</p>
}
</div> </div>
</div> </div>

View File

@@ -11,25 +11,24 @@ namespace MoneyMap.Pages
{ {
private readonly MoneyMapContext _db; private readonly MoneyMapContext _db;
private readonly IReceiptManager _receiptManager; private readonly IReceiptManager _receiptManager;
private readonly IEnumerable<IReceiptParser> _parsers; private readonly IReceiptParseQueue _parseQueue;
private readonly IConfiguration _config; private readonly IConfiguration _config;
public ViewReceiptModel( public ViewReceiptModel(
MoneyMapContext db, MoneyMapContext db,
IReceiptManager receiptManager, IReceiptManager receiptManager,
IEnumerable<IReceiptParser> parsers, IReceiptParseQueue parseQueue,
IConfiguration config) IConfiguration config)
{ {
_db = db; _db = db;
_receiptManager = receiptManager; _receiptManager = receiptManager;
_parsers = parsers; _parseQueue = parseQueue;
_config = config; _config = config;
} }
public Receipt? Receipt { get; set; } public Receipt? Receipt { get; set; }
public List<ReceiptLineItem> LineItems { get; set; } = new(); public List<ReceiptLineItem> LineItems { get; set; } = new();
public List<ReceiptParseLog> ParseLogs { get; set; } = new(); public List<ReceiptParseLog> ParseLogs { get; set; } = new();
public List<ParserOption> AvailableParsers { get; set; } = new();
public string ReceiptUrl { get; set; } = ""; public string ReceiptUrl { get; set; } = "";
public string SelectedModel => _config["AI:ReceiptParsingModel"] ?? "gpt-4o-mini"; public string SelectedModel => _config["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
@@ -62,13 +61,6 @@ namespace MoneyMap.Pages
// Get receipt URL for display - use handler parameter // Get receipt URL for display - use handler parameter
ReceiptUrl = $"/ViewReceipt/{id}?handler=file"; ReceiptUrl = $"/ViewReceipt/{id}?handler=file";
// Get available parsers
AvailableParsers = _parsers.Select(p => new ParserOption
{
Name = p.GetType().Name.Replace("ReceiptParser", ""),
FullName = p.GetType().Name
}).ToList();
return Page(); return Page();
} }
@@ -94,35 +86,26 @@ namespace MoneyMap.Pages
return File(fileBytes, receipt.ContentType); return File(fileBytes, receipt.ContentType);
} }
public async Task<IActionResult> OnPostParseAsync(long id, string parser) public async Task<IActionResult> OnPostParseAsync(long id)
{ {
var selectedParser = _parsers.FirstOrDefault(p => p.GetType().Name == parser); var receipt = await _db.Receipts.FindAsync(id);
if (selectedParser == null) if (receipt == null)
{ {
ErrorMessage = "Parser not found."; ErrorMessage = "Receipt not found.";
return RedirectToPage(new { id }); return RedirectToPage(new { id });
} }
// Use the configured model from settings, pass user notes // Save parsing notes to the receipt entity so the worker can use them
var result = await selectedParser.ParseReceiptAsync(id, SelectedModel, ParsingNotes); receipt.ParsingNotes = ParsingNotes;
receipt.ParseStatus = ReceiptParseStatus.Queued;
await _db.SaveChangesAsync();
if (result.IsSuccess) // Enqueue the receipt for parsing
{ await _parseQueue.EnqueueAsync(id);
SuccessMessage = result.Message;
}
else
{
ErrorMessage = result.Message;
}
SuccessMessage = "Receipt queued for parsing.";
return RedirectToPage(new { id }); return RedirectToPage(new { id });
} }
public class ParserOption
{
public string Name { get; set; } = "";
public string FullName { get; set; } = "";
}
} }
} }

View File

@@ -60,6 +60,9 @@ namespace MoneyMap.Services
if (!File.Exists(filePath)) if (!File.Exists(filePath))
return ReceiptParseResult.Failure("Receipt file not found on disk."); return ReceiptParseResult.Failure("Receipt file not found on disk.");
// Fall back to receipt.ParsingNotes if notes parameter is null
var effectiveNotes = notes ?? receipt.ParsingNotes;
var selectedModel = model ?? _configuration["AI:ReceiptParsingModel"] ?? "gpt-4o-mini"; var selectedModel = model ?? _configuration["AI:ReceiptParsingModel"] ?? "gpt-4o-mini";
var (client, provider) = _clientResolver.Resolve(selectedModel); var (client, provider) = _clientResolver.Resolve(selectedModel);
@@ -79,7 +82,7 @@ namespace MoneyMap.Services
try try
{ {
var (base64Data, mediaType) = await PrepareImageDataAsync(receipt, filePath); var (base64Data, mediaType) = await PrepareImageDataAsync(receipt, filePath);
var promptText = await BuildPromptAsync(receipt, notes, client); var promptText = await BuildPromptAsync(receipt, effectiveNotes, client);
var visionResult = await CallVisionClientAsync(client, base64Data, mediaType, promptText, selectedModel); var visionResult = await CallVisionClientAsync(client, base64Data, mediaType, promptText, selectedModel);
if (!visionResult.IsSuccess) if (!visionResult.IsSuccess)
@@ -89,7 +92,7 @@ namespace MoneyMap.Services
} }
var parseData = ParseResponse(visionResult.Content); var parseData = ParseResponse(visionResult.Content);
await ApplyParseResultAsync(receipt, receiptId, parseData, notes); await ApplyParseResultAsync(receipt, receiptId, parseData, effectiveNotes);
parseLog.Success = true; parseLog.Success = true;
parseLog.Confidence = parseData.Confidence; parseLog.Confidence = parseData.Confidence;