Files
Inboxintel/frontend/src/api/client.js
T
cesnimda 64f835f719 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>
2026-06-30 22:21:10 +02:00

159 lines
7.3 KiB
JavaScript

import axios from 'axios';
// Cookie-based auth (Google OAuth2 session), so send credentials with each call.
const api = axios.create({ baseURL: '/api/v1', withCredentials: true });
api.interceptors.response.use(
(r) => r,
(err) => {
if (err.response?.status === 401) {
// Not signed in. Send the user to the public landing page (unless already
// there) so they can read the features and choose to log in.
if (window.location.pathname !== '/') window.location.href = '/';
}
return Promise.reject(err);
}
);
// Top-level navigation that starts the Google flow and returns to the app.
export const LOGIN_URL = '/api/v1/auth/login?returnUrl=/app';
export const AppApi = {
info: () => api.get('/app/info').then((r) => r.data)
};
export const AuthApi = {
me: () => api.get('/auth/me').then((r) => r.data),
logout: () => api.post('/auth/logout')
};
export const SyncApi = {
status: () => api.get('/sync/status').then((r) => r.data),
full: () => api.post('/sync/full'),
incremental: () => api.post('/sync/incremental')
};
export const AnalyticsApi = {
dashboard: () => api.get('/analytics/dashboard').then((r) => r.data),
health: () => api.get('/analytics/health').then((r) => r.data),
topSenders: (take = 20) => api.get(`/analytics/top-senders?take=${take}`).then((r) => r.data),
volume: (days = 90) => api.get(`/analytics/volume?days=${days}`).then((r) => r.data),
heatmap: () => api.get('/analytics/heatmap').then((r) => r.data),
categoryHeatmap: () => api.get('/analytics/category-heatmap').then((r) => r.data),
attachments: () => api.get('/analytics/attachments').then((r) => r.data),
sidebarCounts: () => api.get('/analytics/sidebar-counts').then((r) => r.data),
// Large take to retrieve all senders for the volume-tiers page.
allSenders: () => api.get('/analytics/top-senders?take=5000').then((r) => r.data)
};
export const CleanupApi = {
preview: (req) => api.post('/cleanup/preview', req).then((r) => r.data),
execute: (req) => api.post('/cleanup/execute', req).then((r) => r.data)
};
// Bulk actions over an explicit set of email IDs (selection-driven, always confirmed —
// the user already opted in by selecting rows and clicking the action).
// Numeric values must match CleanupActionType in Domain/Enums/Enums.cs (no string
// enum converter is configured on the API, so plain numbers are required here).
const CLEANUP_ACTION = { Archive: 0, Trash: 1, MarkRead: 5, MarkUnread: 6, Star: 7, Unstar: 8 };
export const BulkApi = {
markRead: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.MarkRead, emailIds: ids, confirmed: true }),
markUnread: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.MarkUnread, emailIds: ids, confirmed: true }),
star: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Star, emailIds: ids, confirmed: true }),
unstar: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Unstar, emailIds: ids, confirmed: true }),
archive: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Archive, emailIds: ids, confirmed: true }),
trash: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Trash, emailIds: ids, confirmed: true }),
};
export const UnsubscribeApi = {
detect: () => api.post('/unsubscribe/detect'),
safeList: () => api.get('/unsubscribe/safe-list').then((r) => r.data),
process: (req) => api.post('/unsubscribe/process', req).then((r) => r.data)
};
export const LayoutApi = {
get: () => api.get('/widgetlayout').then((r) => r.data),
save: (layout) => api.put('/widgetlayout', layout)
};
export const SearchApi = {
folder: (slug, page = 1, pageSize = 50) =>
api.post('/search', folderToRequest(slug, page, pageSize)).then((r) => r.data),
query: (q, page = 1, pageSize = 50) =>
api.get('/search', { params: { q, page, pageSize } }).then((r) => r.data),
};
function folderToRequest(slug, page, pageSize) {
const base = { page, pageSize };
switch (slug) {
// ── Mailbox ──
case 'inbox': return { ...base, isInInbox: true, isTrashed: false };
case 'allmail': return { ...base };
case 'unread': return { ...base, isUnread: true };
case 'starred': return { ...base, isStarred: true };
case 'sent': return { ...base, gmailLabel: 'SENT' };
case 'drafts': return { ...base, gmailLabel: 'DRAFT' };
case 'archive': return { ...base, isInInbox: false, isTrashed: false };
case 'spam': return { ...base, gmailLabel: 'SPAM' };
case 'trash': return { ...base, isTrashed: true };
// ── Special filters ──
case 'large': return { ...base, minSizeBytes: 5_000_000 };
case 'old': {
const d = new Date(); d.setFullYear(d.getFullYear() - 1);
return { ...base, to: d.toISOString().slice(0, 10) };
}
// ── Smart folders ──
case 'automated': return { ...base, category: 'Notification' };
case 'finance': return { ...base, category: 'Finance' };
case 'social': return { ...base, category: 'Social' };
case 'shopping': return { ...base, category: 'Shopping' };
case 'noreply': return { ...base, query: 'from:noreply' };
case 'gaming': return { ...base, category: 'Gaming' };
case 'sales': return { ...base, category: 'SeasonalSales' };
case 'ridesharing': return { ...base, category: 'RideSharing' };
case 'food': return { ...base, category: 'FoodDelivery' };
case 'wellness': return { ...base, category: 'Wellness' };
// New categories
case 'travel': return { ...base, category: 'Travel' };
case 'subscriptions': return { ...base, category: 'Subscriptions' };
case 'parcels': return { ...base, category: 'Parcels' };
case 'recruitment': return { ...base, category: 'Recruitment' };
case 'events': return { ...base, category: 'Events' };
case 'security': return { ...base, category: 'SecurityAlerts' };
case 'healthcare': return { ...base, category: 'Healthcare' };
case 'education': return { ...base, category: 'Education' };
case 'news': return { ...base, category: 'NewsMedia' };
case 'property': return { ...base, category: 'PropertyUtilities' };
case 'charity': return { ...base, category: 'Charity' };
case 'government': return { ...base, category: 'Government' };
case 'crypto': return { ...base, category: 'CryptoInvesting' };
case 'family': return { ...base, category: 'FamilySchool' };
default: return { ...base };
}
}
export const EmailApi = {
get: (id) => api.get(`/email/${id}`).then((r) => r.data),
summary: (id) => api.get(`/email/${id}/summary`).then((r) => r.data),
markRead: (id) => api.post(`/email/${id}/read`),
markUnread: (id) => api.post(`/email/${id}/unread`),
star: (id) => api.post(`/email/${id}/star`),
unstar: (id) => api.post(`/email/${id}/unstar`),
trash: (id) => api.post(`/email/${id}/trash`),
untrash: (id) => api.post(`/email/${id}/untrash`),
unsubscribe:(id) => api.post(`/email/${id}/unsubscribe`).then((r) => r.data),
};
export const ExportApi = {
reportUrl: (format) => `/api/v1/export/report?format=${format}`
};
export const SettingsApi = {
getDigest: () => api.get('/settings/digest').then((r) => r.data),
setDigest: (enabled) => api.put('/settings/digest', enabled).then((r) => r.data),
sendDigestNow: () => api.post('/settings/digest/send-now'),
};
export default api;