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>
190 lines
8.9 KiB
Plaintext
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"><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>
|
|
}
|