Files
MoneyMap/MoneyMap/Pages/AICategorizePreview.cshtml
AJ Isaacs 444035fd72 Refactor: AICategorizePreview with tabbed proposals and rule status
Split proposals into High Confidence / Needs Review tabs. Extract
proposal table into _ProposalTable partial view. Show rule status
(Create/Update/Exists) based on existing category mappings. Persist
proposals in hidden form field to survive app restarts. Add per-tab
select-all and improved error reporting on apply.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:14:19 -05:00

190 lines
8.9 KiB
Plaintext

@page
@model MoneyMap.Pages.AICategorizePreviewModel
@{
ViewData["Title"] = "AI Categorization Preview";
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>AI Categorization Preview</h2>
<div>
<a asp-page="/Recategorize" class="btn btn-outline-secondary">Back to Recategorize</a>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@Model.ErrorMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
}
@if (!Model.Proposals.Any())
{
<div class="card shadow-sm">
<div class="card-header">
<strong>Generate AI Suggestions</strong>
</div>
<div class="card-body">
@if (Model.TransactionIds != null && Model.TransactionIds.Length > 0)
{
<p>Generate AI categorization suggestions for <strong>@Model.SelectedTransactionCount selected transaction(s)</strong>. You can review and approve them before applying.</p>
<form method="post" asp-page-handler="GenerateForIds"
onsubmit="this.querySelector('button[type=submit]').disabled = true; this.querySelector('button[type=submit]').innerHTML = '<span class=\'spinner-border spinner-border-sm me-2\'></span>Analyzing transactions...';">
@foreach (var id in Model.TransactionIds!)
{
<input type="hidden" name="transactionIds" value="@id" />
}
<p class="text-muted small mb-3">
Using: <strong>@Model.SelectedModel</strong>
<a href="/Settings" class="ms-2 small">Change</a>
</p>
<button type="submit" class="btn btn-primary">
Generate Suggestions for @Model.SelectedTransactionCount Transaction(s)
</button>
</form>
}
else
{
<p>Generate AI categorization suggestions for uncategorized transactions. You can review and approve them before applying.</p>
<form method="post" asp-page-handler="Generate"
onsubmit="this.querySelector('button[type=submit]').disabled = true; this.querySelector('button[type=submit]').innerHTML = '<span class=\'spinner-border spinner-border-sm me-2\'></span>Analyzing transactions...';">
<p class="text-muted small mb-3">
Using: <strong>@Model.SelectedModel</strong>
<a href="/Settings" class="ms-2 small">Change</a>
</p>
<button type="submit" class="btn btn-primary">
Generate Suggestions (up to 50 uncategorized)
</button>
</form>
}
</div>
</div>
}
else
{
var highConfidence = Model.Proposals.Where(p => p.Confidence >= 0.8m).ToList();
var needsReview = Model.Proposals.Where(p => p.Confidence < 0.8m).ToList();
<form method="post" asp-page-handler="Apply">
<input type="hidden" name="proposalsData" value="@Model.ProposalsJson" />
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Review Proposals (@Model.Proposals.Count suggestions)</strong>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll(true)">Select All</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll(false)">Deselect All</button>
</div>
</div>
<div class="card-body p-0">
<ul class="nav nav-tabs px-3 pt-3" id="proposalTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="high-confidence-tab" data-bs-toggle="tab"
data-bs-target="#high-confidence" type="button" role="tab">
High Confidence <span class="badge bg-success ms-1">@highConfidence.Count</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="needs-review-tab" data-bs-toggle="tab"
data-bs-target="#needs-review" type="button" role="tab">
Needs Review <span class="badge bg-warning text-dark ms-1">@needsReview.Count</span>
</button>
</li>
</ul>
<div class="tab-content" id="proposalTabContent">
<div class="tab-pane fade show active" id="high-confidence" role="tabpanel">
@if (highConfidence.Any())
{
@await Html.PartialAsync("_ProposalTable", highConfidence)
}
else
{
<div class="p-4 text-center text-muted">No high confidence proposals.</div>
}
</div>
<div class="tab-pane fade" id="needs-review" role="tabpanel">
@if (needsReview.Any())
{
@await Html.PartialAsync("_ProposalTable", needsReview)
}
else
{
<div class="p-4 text-center text-muted">No proposals needing review.</div>
}
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between">
<a asp-page="/Recategorize" class="btn btn-outline-secondary">Cancel</a>
<button type="submit" class="btn btn-success">
Apply Selected Categorizations
</button>
</div>
</div>
</form>
<div class="card shadow-sm">
<div class="card-header">
<strong>Legend</strong>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Confidence Levels</h6>
<ul class="list-unstyled mb-0">
<li><span class="badge bg-success">80%+</span> High confidence - likely correct</li>
<li><span class="badge bg-warning text-dark">60-79%</span> Medium confidence - review recommended</li>
<li><span class="badge bg-secondary">&lt;60%</span> Low confidence - verify carefully</li>
</ul>
</div>
<div class="col-md-6">
<h6>Rule Status</h6>
<ul class="list-unstyled mb-0">
<li><span class="small text-muted">Create</span> - No existing rule; check to create a new mapping rule</li>
<li><span class="badge bg-warning text-dark">Update</span> - Pattern exists with a different category; check to update it</li>
<li><span class="badge bg-info text-dark">Exists</span> - Rule already exists with the same category</li>
</ul>
</div>
</div>
</div>
</div>
}
@section Scripts {
<script>
// Select/deselect all proposals across both tabs
function selectAll(checked) {
document.querySelectorAll('.proposal-checkbox').forEach(cb => {
cb.checked = checked;
if (!checked) {
var ruleCheckbox = cb.closest('tr').querySelector('.create-rule-checkbox');
if (ruleCheckbox) ruleCheckbox.checked = false;
}
});
document.querySelectorAll('.select-all-tab').forEach(cb => cb.checked = checked);
}
// Select/deselect all within a single tab
function selectAllInTab(headerCheckbox) {
var table = headerCheckbox.closest('table');
table.querySelectorAll('.proposal-checkbox').forEach(cb => {
cb.checked = headerCheckbox.checked;
if (!headerCheckbox.checked) {
var ruleCheckbox = cb.closest('tr').querySelector('.create-rule-checkbox');
if (ruleCheckbox) ruleCheckbox.checked = false;
}
});
}
// When a proposal checkbox is unchecked, also uncheck its create-rule checkbox
document.addEventListener('change', function (e) {
if (e.target.classList.contains('proposal-checkbox') && !e.target.checked) {
var ruleCheckbox = e.target.closest('tr').querySelector('.create-rule-checkbox');
if (ruleCheckbox) ruleCheckbox.checked = false;
}
});
</script>
}