Cleanup: Remove redundant AI categorization pages

ReviewAISuggestions and ReviewAISuggestionsWithProposals were a two-page
workflow superseded by AICategorizePreview, which handles batch approval,
tabs, and TempData storage in a single page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 20:02:50 -05:00
parent 4be5658d32
commit b5f46a7646
4 changed files with 0 additions and 565 deletions

View File

@@ -1,126 +0,0 @@
@page
@model MoneyMap.Pages.ReviewAISuggestionsModel
@{
ViewData["Title"] = "AI Categorization Suggestions";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
("AI Suggestions", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>AI Categorization Suggestions</h2>
<div class="d-flex gap-2">
<a asp-page="/Transactions" asp-route-category="(blank)" class="btn btn-outline-secondary">
View Uncategorized
</a>
<a asp-page="/Index" class="btn btn-outline-secondary">Back to Dashboard</a>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@Model.SuccessMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</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>
}
<div class="card shadow-sm mb-3">
<div class="card-body">
<h5 class="card-title">How AI Categorization Works</h5>
<p class="card-text">
This tool uses AI to analyze your uncategorized transactions and suggest:
</p>
<ul>
<li><strong>Category</strong> - The most appropriate expense category</li>
<li><strong>Merchant Name</strong> - A normalized merchant name (e.g., "Walmart" from "WAL-MART #1234")</li>
<li><strong>Pattern Rule</strong> - An optional rule to auto-categorize similar transactions in the future</li>
</ul>
<p class="card-text">
<strong>Cost:</strong> Approximately $0.00015 per transaction (~1.5 cents per 100 transactions)
</p>
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Uncategorized Transactions (@Model.TotalUncategorized)</strong>
<form method="post" asp-page-handler="GenerateSuggestions" class="d-inline">
<button type="submit" class="btn btn-primary btn-sm">
Generate AI Suggestions (up to 20)
</button>
</form>
</div>
<div class="card-body">
@if (!Model.Transactions.Any())
{
<p class="text-muted">No uncategorized transactions found. Great job!</p>
}
else
{
<p class="text-muted mb-3">
Showing the @Model.Transactions.Count most recent uncategorized transactions.
Click "Generate AI Suggestions" to analyze up to 20 transactions.
</p>
<div class="table-responsive">
<table class="table table-hover table-sm">
<thead>
<tr>
<th>Date</th>
<th>Name</th>
<th>Memo</th>
<th class="text-end">Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Transactions)
{
<tr>
<td>@item.Transaction.Date.ToString("yyyy-MM-dd")</td>
<td>@item.Transaction.Name</td>
<td class="text-truncate" style="max-width: 300px;">@item.Transaction.Memo</td>
<td class="text-end">
<span class="@(item.Transaction.Amount < 0 ? "text-danger" : "text-success")">
@item.Transaction.Amount.ToString("C")
</span>
</td>
<td>
<a asp-page="/EditTransaction" asp-route-id="@item.Transaction.Id" class="btn btn-sm btn-outline-primary">
Edit
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<strong>Quick Tips</strong>
</div>
<div class="card-body">
<ul class="small mb-0">
<li>AI suggestions are based on transaction name, memo, amount, and date</li>
<li>You can accept, reject, or modify each suggestion</li>
<li>Creating rules helps auto-categorize future transactions</li>
<li>High confidence suggestions (>80%) are more reliable</li>
<li>You can manually edit any transaction from the Transactions page</li>
</ul>
</div>
</div>

View File

@@ -1,114 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
using MoneyMap.Services;
namespace MoneyMap.Pages;
public class ReviewAISuggestionsModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly ITransactionAICategorizer _aiCategorizer;
public ReviewAISuggestionsModel(MoneyMapContext db, ITransactionAICategorizer aiCategorizer)
{
_db = db;
_aiCategorizer = aiCategorizer;
}
public List<TransactionWithProposal> Transactions { get; set; } = new();
public bool IsGenerating { get; set; }
public int TotalUncategorized { get; set; }
[TempData]
public string? SuccessMessage { get; set; }
[TempData]
public string? ErrorMessage { get; set; }
public async Task OnGetAsync()
{
// Get uncategorized transactions
var uncategorized = await _db.Transactions
.Include(t => t.Merchant)
.Where(t => string.IsNullOrEmpty(t.Category))
.OrderByDescending(t => t.Date)
.Take(50) // Limit to 50 most recent
.ToListAsync();
TotalUncategorized = uncategorized.Count;
Transactions = uncategorized.Select(t => new TransactionWithProposal
{
Transaction = t,
Proposal = null // Will be populated via AJAX or on generate
}).ToList();
}
public async Task<IActionResult> OnPostGenerateSuggestionsAsync()
{
// Get uncategorized transactions
var uncategorized = await _db.Transactions
.Where(t => string.IsNullOrEmpty(t.Category))
.OrderByDescending(t => t.Date)
.Take(20) // Limit to 20 for cost control
.ToListAsync();
if (!uncategorized.Any())
{
ErrorMessage = "No uncategorized transactions found.";
return RedirectToPage();
}
// Generate proposals
var proposals = await _aiCategorizer.ProposeBatchCategorizationAsync(uncategorized);
// Store proposals in session for review
HttpContext.Session.SetString("AIProposals", System.Text.Json.JsonSerializer.Serialize(proposals));
SuccessMessage = $"Generated {proposals.Count} AI suggestions. Review them below.";
return RedirectToPage("ReviewAISuggestionsWithProposals");
}
public async Task<IActionResult> OnPostApplyProposalAsync(long transactionId, string category, string? merchant, string? pattern, decimal confidence, bool createRule)
{
var proposal = new AICategoryProposal
{
TransactionId = transactionId,
Category = category,
CanonicalMerchant = merchant,
Pattern = pattern,
Confidence = confidence,
CreateRule = createRule
};
var result = await _aiCategorizer.ApplyProposalAsync(transactionId, proposal, createRule);
if (result.Success)
{
SuccessMessage = result.RuleCreated
? "Transaction categorized and rule created!"
: "Transaction categorized!";
}
else
{
ErrorMessage = result.ErrorMessage ?? "Failed to apply suggestion.";
}
return RedirectToPage();
}
public IActionResult OnPostRejectProposalAsync(long transactionId)
{
// Just refresh the page, removing this transaction from view
SuccessMessage = "Suggestion rejected.";
return RedirectToPage();
}
public class TransactionWithProposal
{
public Transaction Transaction { get; set; } = null!;
public AICategoryProposal? Proposal { get; set; }
}
}

View File

@@ -1,168 +0,0 @@
@page
@model MoneyMap.Pages.ReviewAISuggestionsWithProposalsModel
@{
ViewData["Title"] = "Review AI Suggestions";
ViewData["Breadcrumbs"] = new List<(string Label, string? Url)>
{
("Transactions", Url.Page("/Transactions")),
("AI Suggestions", Url.Page("/ReviewAISuggestions")),
("Review Proposals", null)
};
}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>Review AI Suggestions</h2>
<div class="d-flex gap-2">
<form method="post" asp-page-handler="ApplyAll" class="d-inline" onsubmit="return confirm('Apply all high-confidence suggestions (≥80%)?');">
<button type="submit" class="btn btn-success">
Apply All High Confidence
</button>
</form>
<a asp-page="/ReviewAISuggestions" class="btn btn-outline-secondary">Back</a>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
@Model.SuccessMessage
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</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="alert alert-info">
<h5>No suggestions remaining</h5>
<p class="mb-0">
All AI suggestions have been processed.
<a asp-page="/ReviewAISuggestions" class="alert-link">Generate more suggestions</a>
</p>
</div>
}
else
{
<div class="alert alert-info mb-3">
<strong>Review each suggestion below.</strong> You can accept the AI's proposal, reject it, or modify it before applying.
High confidence suggestions (≥80%) are generally very reliable.
</div>
@foreach (var item in Model.Proposals)
{
var confidenceClass = item.Proposal.Confidence >= 0.8m ? "success" :
item.Proposal.Confidence >= 0.6m ? "warning" : "danger";
var confidencePercent = (item.Proposal.Confidence * 100).ToString("F0");
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>@item.Transaction.Name</strong>
<span class="text-muted ms-2">@item.Transaction.Date.ToString("yyyy-MM-dd")</span>
<span class="badge bg-@confidenceClass ms-2">@confidencePercent% Confidence</span>
</div>
<span class="@(item.Transaction.Amount < 0 ? "text-danger" : "text-success") fw-bold">
@item.Transaction.Amount.ToString("C")
</span>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Transaction Details</h6>
<dl class="row small">
<dt class="col-sm-3">Memo:</dt>
<dd class="col-sm-9">@item.Transaction.Memo</dd>
<dt class="col-sm-3">Amount:</dt>
<dd class="col-sm-9">@item.Transaction.Amount.ToString("C")</dd>
</dl>
</div>
<div class="col-md-6">
<h6>AI Suggestion</h6>
<dl class="row small">
<dt class="col-sm-4">Category:</dt>
<dd class="col-sm-8"><span class="badge bg-primary">@item.Proposal.Category</span></dd>
@if (!string.IsNullOrWhiteSpace(item.Proposal.CanonicalMerchant))
{
<dt class="col-sm-4">Merchant:</dt>
<dd class="col-sm-8">@item.Proposal.CanonicalMerchant</dd>
}
@if (!string.IsNullOrWhiteSpace(item.Proposal.Pattern))
{
<dt class="col-sm-4">Pattern:</dt>
<dd class="col-sm-8"><code>@item.Proposal.Pattern</code></dd>
}
@if (!string.IsNullOrWhiteSpace(item.Proposal.Reasoning))
{
<dt class="col-sm-4">Reasoning:</dt>
<dd class="col-sm-8"><em>@item.Proposal.Reasoning</em></dd>
}
</dl>
</div>
</div>
<hr />
<div class="d-flex gap-2 justify-content-end">
<form method="post" asp-page-handler="RejectProposal" class="d-inline">
<input type="hidden" name="transactionId" value="@item.Transaction.Id" />
<button type="submit" class="btn btn-outline-danger btn-sm">
Reject
</button>
</form>
<form method="post" asp-page-handler="ApplyProposal" class="d-inline">
<input type="hidden" name="transactionId" value="@item.Transaction.Id" />
<input type="hidden" name="category" value="@item.Proposal.Category" />
<input type="hidden" name="merchant" value="@item.Proposal.CanonicalMerchant" />
<input type="hidden" name="pattern" value="@item.Proposal.Pattern" />
<input type="hidden" name="confidence" value="@item.Proposal.Confidence" />
<input type="hidden" name="createRule" value="false" />
<button type="submit" class="btn btn-outline-primary btn-sm">
Apply (No Rule)
</button>
</form>
<form method="post" asp-page-handler="ApplyProposal" class="d-inline">
<input type="hidden" name="transactionId" value="@item.Transaction.Id" />
<input type="hidden" name="category" value="@item.Proposal.Category" />
<input type="hidden" name="merchant" value="@item.Proposal.CanonicalMerchant" />
<input type="hidden" name="pattern" value="@item.Proposal.Pattern" />
<input type="hidden" name="confidence" value="@item.Proposal.Confidence" />
<input type="hidden" name="createRule" value="true" />
<button type="submit" class="btn btn-primary btn-sm">
Apply + Create Rule
</button>
</form>
<a asp-page="/EditTransaction" asp-route-id="@item.Transaction.Id" class="btn btn-outline-secondary btn-sm">
Edit Manually
</a>
</div>
</div>
</div>
}
}
<div class="card shadow-sm mt-3">
<div class="card-header">
<strong>Understanding Confidence Scores</strong>
</div>
<div class="card-body">
<ul class="small mb-0">
<li><span class="badge bg-success">≥80%</span> - High confidence, very reliable</li>
<li><span class="badge bg-warning text-dark">60-79%</span> - Medium confidence, review recommended</li>
<li><span class="badge bg-danger">&lt;60%</span> - Low confidence, manual review strongly recommended</li>
</ul>
</div>
</div>

View File

@@ -1,157 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
using MoneyMap.Services;
using System.Text.Json;
namespace MoneyMap.Pages;
public class ReviewAISuggestionsWithProposalsModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly ITransactionAICategorizer _aiCategorizer;
public ReviewAISuggestionsWithProposalsModel(MoneyMapContext db, ITransactionAICategorizer aiCategorizer)
{
_db = db;
_aiCategorizer = aiCategorizer;
}
public List<TransactionWithProposal> Proposals { get; set; } = new();
[TempData]
public string? SuccessMessage { get; set; }
[TempData]
public string? ErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync()
{
// Load proposals from session
var proposalsJson = HttpContext.Session.GetString("AIProposals");
if (string.IsNullOrWhiteSpace(proposalsJson))
{
ErrorMessage = "No AI suggestions found. Please generate suggestions first.";
return RedirectToPage("ReviewAISuggestions");
}
var proposals = JsonSerializer.Deserialize<List<AICategoryProposal>>(proposalsJson);
if (proposals == null || !proposals.Any())
{
ErrorMessage = "Failed to load AI suggestions.";
return RedirectToPage("ReviewAISuggestions");
}
// Load transactions for these proposals
var transactionIds = proposals.Select(p => p.TransactionId).ToList();
var transactions = await _db.Transactions
.Include(t => t.Merchant)
.Where(t => transactionIds.Contains(t.Id))
.ToListAsync();
Proposals = proposals.Select(p => new TransactionWithProposal
{
Transaction = transactions.FirstOrDefault(t => t.Id == p.TransactionId)!,
Proposal = p
}).Where(x => x.Transaction != null).ToList();
return Page();
}
public async Task<IActionResult> OnPostApplyProposalAsync(long transactionId, string category, string? merchant, string? pattern, decimal confidence, bool createRule)
{
var proposal = new AICategoryProposal
{
TransactionId = transactionId,
Category = category,
CanonicalMerchant = merchant,
Pattern = pattern,
Confidence = confidence,
CreateRule = createRule
};
var result = await _aiCategorizer.ApplyProposalAsync(transactionId, proposal, createRule);
if (result.Success)
{
// Remove this proposal from session
var proposalsJson = HttpContext.Session.GetString("AIProposals");
if (!string.IsNullOrWhiteSpace(proposalsJson))
{
var proposals = JsonSerializer.Deserialize<List<AICategoryProposal>>(proposalsJson);
if (proposals != null)
{
proposals.RemoveAll(p => p.TransactionId == transactionId);
HttpContext.Session.SetString("AIProposals", JsonSerializer.Serialize(proposals));
}
}
SuccessMessage = result.RuleCreated
? "Transaction categorized and rule created!"
: "Transaction categorized!";
}
else
{
ErrorMessage = result.ErrorMessage ?? "Failed to apply suggestion.";
}
return RedirectToPage();
}
public IActionResult OnPostRejectProposalAsync(long transactionId)
{
// Remove this proposal from session
var proposalsJson = HttpContext.Session.GetString("AIProposals");
if (!string.IsNullOrWhiteSpace(proposalsJson))
{
var proposals = JsonSerializer.Deserialize<List<AICategoryProposal>>(proposalsJson);
if (proposals != null)
{
proposals.RemoveAll(p => p.TransactionId == transactionId);
HttpContext.Session.SetString("AIProposals", JsonSerializer.Serialize(proposals));
}
}
SuccessMessage = "Suggestion rejected.";
return RedirectToPage();
}
public async Task<IActionResult> OnPostApplyAllAsync()
{
var proposalsJson = HttpContext.Session.GetString("AIProposals");
if (string.IsNullOrWhiteSpace(proposalsJson))
{
ErrorMessage = "No AI suggestions found.";
return RedirectToPage("ReviewAISuggestions");
}
var proposals = JsonSerializer.Deserialize<List<AICategoryProposal>>(proposalsJson);
if (proposals == null || !proposals.Any())
{
ErrorMessage = "Failed to load AI suggestions.";
return RedirectToPage("ReviewAISuggestions");
}
int applied = 0;
foreach (var proposal in proposals.Where(p => p.Confidence >= 0.8m))
{
var result = await _aiCategorizer.ApplyProposalAsync(proposal.TransactionId, proposal, proposal.CreateRule);
if (result.Success)
applied++;
}
// Clear session
HttpContext.Session.Remove("AIProposals");
SuccessMessage = $"Applied {applied} high-confidence suggestions (≥80%).";
return RedirectToPage("ReviewAISuggestions");
}
public class TransactionWithProposal
{
public Transaction Transaction { get; set; } = null!;
public AICategoryProposal Proposal { get; set; } = null!;
}
}