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
+23
View File
@@ -71,10 +71,13 @@ function SenderList({ senders, selectedId, onSelect, search, onSearch }) {
function EmailDetail({ email: summary, onBack }) {
const [detail, setDetail] = useState(null);
const [loading, setLoading] = useState(true);
const [aiSummary, setAiSummary] = useState(null);
const [aiLoading, setAiLoading] = useState(false);
useEffect(() => {
setDetail(null);
setLoading(true);
setAiSummary(null);
EmailApi.get(summary.id)
.then(setDetail)
.catch(() => setDetail(null))
@@ -83,6 +86,14 @@ function EmailDetail({ email: summary, onBack }) {
const email = detail ?? summary;
const fetchAiSummary = () => {
setAiLoading(true);
EmailApi.summary(summary.id)
.then((r) => setAiSummary(r.summary))
.catch(() => setAiSummary(null))
.finally(() => setAiLoading(false));
};
return (
<div className="sd-detail">
<button className="sd-back" onClick={onBack}> Back to list</button>
@@ -101,6 +112,18 @@ function EmailDetail({ email: summary, onBack }) {
</div>
</div>
{!loading && (
<div className="sd-ai-summary">
{aiSummary != null ? (
<div className="sd-ai-summary-text"> {aiSummary}</div>
) : (
<button className="btn-sm" onClick={fetchAiSummary} disabled={aiLoading}>
{aiLoading ? 'Summarising…' : '✨ AI summary'}
</button>
)}
</div>
)}
{loading && <div className="sd-body-loading muted">Loading message</div>}
{!loading && detail?.bodyText && (