feat: AI Control Settings mit Category Control und Company Labels

MAJOR FEATURES:
- AI Control Tab in Settings hinzugefügt mit vollständiger KI-Steuerung
- Category Control: Benutzer können Kategorien aktivieren/deaktivieren und Aktionen pro Kategorie festlegen (Keep in Inbox, Archive & Mark Read, Star)
- Company Labels: Automatische Erkennung bekannter Firmen (Amazon, Google, Microsoft, etc.) und optionale benutzerdefinierte Company Labels
- Auto-Detect Companies Toggle: Automatische Label-Erstellung für bekannte Firmen

UI/UX VERBESSERUNGEN:
- Sorting Rules Tab entfernt (war zu verwirrend)
- Save Buttons nach oben rechts verschoben (Category Control und Company Labels)
- Company Labels Section: Custom Labels sind jetzt in einem ausklappbaren Details-Element (Optional)
- Verbesserte Beschreibungen und Klarheit in der UI

BACKEND ÄNDERUNGEN:
- Neue API Endpoints: /api/preferences/ai-control (GET/POST) und /api/preferences/company-labels (GET/POST/DELETE)
- AI Sorter Service erweitert: detectCompany(), matchesCompanyLabel(), getCategoryAction(), getEnabledCategories()
- Database Service: Default-Werte und Merge-Logik für erweiterte User Preferences
- Email Routes: Integration der neuen AI Control Einstellungen in Gmail und Outlook Sortierung
- Label-Erstellung: Nur für enabledCategories, Custom Company Labels mit orange Farbe (#ff9800)

FRONTEND ÄNDERUNGEN:
- Neue TypeScript Types: client/src/types/settings.ts (AIControlSettings, CompanyLabel, CategoryInfo, KnownCompany)
- Settings.tsx: Komplett überarbeitet mit AI Control Tab, Category Toggles, Company Labels Management
- API Client erweitert: getAIControlSettings(), saveAIControlSettings(), getCompanyLabels(), saveCompanyLabel(), deleteCompanyLabel()
- Debug-Logs hinzugefügt für Troubleshooting (main.tsx, App.tsx, Settings.tsx)

BUGFIXES:
- JSX Syntax-Fehler behoben: Fehlende schließende </div> Tags in Company Labels Section
- TypeScript Typ-Fehler behoben: saved.data null-check für Company Labels
- Struktur-Fehler behoben: Conditional Blocks korrekt verschachtelt

TECHNISCHE DETAILS:
- 9 Kategorien verfügbar: VIP, Clients, Invoices, Newsletter, Promotions, Social, Security, Calendar, Review
- Company Labels unterstützen Bedingungen wie 'from:amazon.com OR from:amazon.de'
- Priorisierung: 1) Custom Company Labels, 2) Auto-Detected Companies, 3) AI Categorization
- Deaktivierte Kategorien werden automatisch als 'review' kategorisiert
This commit is contained in:
2026-01-26 17:49:39 +01:00
parent 6ba5563d54
commit 18c11d27bc
9 changed files with 963 additions and 86 deletions

View File

@@ -379,11 +379,17 @@ router.post('/sort',
log.success(`${deletedLabels} old labels cleaned up`)
}
// Create labels only for categories without native Gmail category
// Create labels for categories and company labels
const categories = sorter.getCategories()
const labelMap = {}
const companyLabelMap = {}
// Create labels for enabled categories only
const enabledCategories = sorter.getEnabledCategories(preferences)
for (const [key, cat] of Object.entries(categories)) {
// Skip disabled categories
if (!enabledCategories.includes(key)) continue
// Wenn Gmail-Kategorie existiert, diese verwenden
const gmailCat = sorter.getGmailCategory(key)
if (gmailCat) {
@@ -401,6 +407,38 @@ router.post('/sort',
}
}
// Create company labels
if (preferences.companyLabels?.length) {
for (const companyLabel of preferences.companyLabels) {
if (!companyLabel.enabled) continue
try {
// Use orange color for company labels
const label = await gmail.createLabel(companyLabel.name, '#ff9800')
if (label) {
companyLabelMap[companyLabel.id || companyLabel.name] = label.id
}
} catch (err) {
log.warn(`Failed to create company label: ${companyLabel.name}`)
}
}
}
// Create auto-detected company labels if enabled
if (preferences.autoDetectCompanies) {
const knownCompanies = ['Amazon', 'Google', 'Microsoft', 'Apple', 'Facebook', 'Twitter', 'LinkedIn', 'GitHub', 'Netflix', 'Spotify', 'PayPal', 'Stripe', 'Shopify', 'Uber', 'Airbnb', 'Dropbox', 'Slack', 'Zoom']
for (const companyName of knownCompanies) {
try {
const label = await gmail.createLabel(companyName, '#ff9800')
if (label) {
companyLabelMap[companyName] = label.id
}
} catch (err) {
// Label might already exist, ignore
}
}
}
// Fetch and process ALL emails with pagination
let pageToken = null
let totalProcessed = 0
@@ -420,27 +458,76 @@ router.post('/sort',
// Get full email details
const emails = await gmail.batchGetEmails(messages.map(m => m.id))
// Classify emails with AI
const classified = await sorter.batchCategorize(
emails.map(e => ({
from: e.headers?.from || '',
subject: e.headers?.subject || '',
snippet: e.snippet || '',
})),
preferences
)
// Process each email: check company labels first, then AI categorization
const processedEmails = []
for (const email of emails) {
const emailData = {
from: email.headers?.from || '',
subject: email.headers?.subject || '',
snippet: email.snippet || '',
}
let category = null
let companyLabel = null
let skipAI = false
// PRIORITY 1: Check custom company labels
if (preferences.companyLabels?.length) {
for (const companyLabelConfig of preferences.companyLabels) {
if (!companyLabelConfig.enabled) continue
if (sorter.matchesCompanyLabel(emailData, companyLabelConfig)) {
category = companyLabelConfig.category || 'promotions'
companyLabel = companyLabelConfig.name
skipAI = true
break
}
}
}
// PRIORITY 2: Check auto-detected companies
if (!skipAI && preferences.autoDetectCompanies) {
const detected = sorter.detectCompany(emailData)
if (detected) {
category = 'promotions' // Default category for companies
companyLabel = detected.label
skipAI = true
}
}
// PRIORITY 3: AI categorization (if no company label matched)
if (!skipAI) {
const classification = await sorter.categorize(emailData, preferences)
category = classification.category
// If category is disabled, fallback to review
if (!enabledCategories.includes(category)) {
category = 'review'
}
}
processedEmails.push({
email,
category,
companyLabel,
})
}
// Apply labels/categories and actions
for (let i = 0; i < classified.length; i++) {
const email = emails[i]
const { category } = classified[i].classification
const action = sorter.getCategoryAction(category)
for (const { email, category, companyLabel } of processedEmails) {
const action = sorter.getCategoryAction(category, preferences)
try {
const labelsToAdd = []
const labelsToRemove = []
// Add label/category
// Add company label if matched
if (companyLabel && companyLabelMap[companyLabel]) {
labelsToAdd.push(companyLabelMap[companyLabel])
}
// Add category label/category
if (labelMap[category]) {
labelsToAdd.push(labelMap[category])
}
@@ -533,24 +620,75 @@ router.post('/sort',
if (!messages?.length) break
// Classify emails with AI
const classified = await sorter.batchCategorize(
messages.map(e => ({
from: e.from?.emailAddress?.address || '',
subject: e.subject || '',
snippet: e.bodyPreview || '',
})),
preferences
)
// Process each email: check company labels first, then AI categorization
const enabledCategories = sorter.getEnabledCategories(preferences)
const processedEmails = []
for (const email of messages) {
const emailData = {
from: email.from?.emailAddress?.address || '',
subject: email.subject || '',
snippet: email.bodyPreview || '',
}
let category = null
let companyLabel = null
let skipAI = false
// PRIORITY 1: Check custom company labels
if (preferences.companyLabels?.length) {
for (const companyLabelConfig of preferences.companyLabels) {
if (!companyLabelConfig.enabled) continue
if (sorter.matchesCompanyLabel(emailData, companyLabelConfig)) {
category = companyLabelConfig.category || 'promotions'
companyLabel = companyLabelConfig.name
skipAI = true
break
}
}
}
// PRIORITY 2: Check auto-detected companies
if (!skipAI && preferences.autoDetectCompanies) {
const detected = sorter.detectCompany(emailData)
if (detected) {
category = 'promotions' // Default category for companies
companyLabel = detected.label
skipAI = true
}
}
// PRIORITY 3: AI categorization (if no company label matched)
if (!skipAI) {
const classification = await sorter.categorize(emailData, preferences)
category = classification.category
// If category is disabled, fallback to review
if (!enabledCategories.includes(category)) {
category = 'review'
}
}
processedEmails.push({
email,
category,
companyLabel,
})
}
// Apply categories and actions
for (let i = 0; i < classified.length; i++) {
const email = messages[i]
const { category } = classified[i].classification
const action = sorter.getCategoryAction(category)
for (const { email, category, companyLabel } of processedEmails) {
const action = sorter.getCategoryAction(category, preferences)
const catName = sorter.getLabelName(category)
try {
// Add company label as category if matched
if (companyLabel) {
await outlook.addCategories(email.id, [companyLabel])
}
// Add category
await outlook.addCategories(email.id, [catName])
// Handle different actions