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:
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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"><60%</span> - Low confidence, manual review strongly recommended</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user