feat: AI categorisation fallback, unsubscribe confidence scoring, one-line summaries

Heuristic classifier falls back to AI only when it can't determine a category;
unsubscribe confidence blends method reliability with an AI safety opinion and
surfaces it in the Unsubscribe Manager; emails get an on-demand AI one-line
summary in the detail pane. All AI calls degrade gracefully when AI is disabled
or the provider errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-06-30 22:21:10 +02:00
parent 0163165beb
commit 64f835f719
14 changed files with 949 additions and 10 deletions
@@ -18,12 +18,14 @@ public class EmailController : ApiControllerBase
{
private readonly ICleanupService _cleanup;
private readonly IUnsubscribeService _unsub;
private readonly IAiService _ai;
private readonly AppDbContext _db;
public EmailController(ICleanupService cleanup, IUnsubscribeService unsub, AppDbContext db)
public EmailController(ICleanupService cleanup, IUnsubscribeService unsub, IAiService ai, AppDbContext db)
{
_cleanup = cleanup;
_unsub = unsub;
_ai = ai;
_db = db;
}
@@ -42,6 +44,16 @@ public class EmailController : ApiControllerBase
return e is null ? NotFound() : Ok(e);
}
/// <summary>One-line AI summary, fetched on demand (returns the snippet verbatim if AI is disabled).</summary>
[HttpGet("{id:guid}/summary")]
public async Task<IActionResult> Summary(Guid id, CancellationToken ct)
{
var e = await _db.Emails.FirstOrDefaultAsync(e => e.Id == id && e.UserId == UserId, ct);
if (e is null) return NotFound();
var summary = await _ai.SummarizeEmailAsync(e.Subject, e.Snippet, e.BodyText, ct);
return Ok(new { summary });
}
[HttpPost("{id:guid}/read")]
public Task<IActionResult> MarkRead(Guid id, CancellationToken ct) => Act(id, CleanupActionType.MarkRead, ct);