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

@@ -17,6 +17,12 @@ import { Imprint } from '@/pages/Imprint'
// Initialize analytics on app startup
initAnalytics()
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.tsx:18',message:'App.tsx loaded, importing Settings',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
} catch(e) {}
// #endregion
// Loading spinner component
function LoadingSpinner() {
return (
@@ -63,6 +69,12 @@ function AppRoutes() {
// Track page views on route changes
usePageTracking()
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.tsx:64',message:'AppRoutes rendering',data:{pathname:window.location.pathname},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
} catch(e) {}
// #endregion
return (
<Routes>
{/* Public pages */}

View File

@@ -270,6 +270,68 @@ export const api = {
})
},
// ═══════════════════════════════════════════════════════════════════════════
// AI CONTROL
// ═══════════════════════════════════════════════════════════════════════════
async getAIControlSettings(userId: string) {
return fetchApi<{
enabledCategories: string[]
categoryActions: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies: boolean
}>(`/preferences/ai-control?userId=${userId}`)
},
async saveAIControlSettings(userId: string, settings: {
enabledCategories?: string[]
categoryActions?: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies?: boolean
}) {
return fetchApi<{ success: boolean }>('/preferences/ai-control', {
method: 'POST',
body: JSON.stringify({ userId, ...settings }),
})
},
// ═══════════════════════════════════════════════════════════════════════════
// COMPANY LABELS
// ═══════════════════════════════════════════════════════════════════════════
async getCompanyLabels(userId: string) {
return fetchApi<Array<{
id?: string
name: string
condition: string
enabled: boolean
category?: string
}>>(`/preferences/company-labels?userId=${userId}`)
},
async saveCompanyLabel(userId: string, companyLabel: {
id?: string
name: string
condition: string
enabled: boolean
category?: string
}) {
return fetchApi<{
id?: string
name: string
condition: string
enabled: boolean
category?: string
}>('/preferences/company-labels', {
method: 'POST',
body: JSON.stringify({ userId, companyLabel }),
})
},
async deleteCompanyLabel(userId: string, labelId: string) {
return fetchApi<{ success: boolean }>(`/preferences/company-labels/${labelId}?userId=${userId}`, {
method: 'DELETE',
})
},
// ═══════════════════════════════════════════════════════════════════════════
// PRODUCTS & QUESTIONS (Legacy)
// ═══════════════════════════════════════════════════════════════════════════

View File

@@ -3,6 +3,12 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.tsx:8',message:'App starting',data:{rootExists:!!document.getElementById('root')},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
} catch(e) {}
// #endregion
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />

View File

@@ -22,9 +22,12 @@ import {
Loader2,
Crown,
Star,
Brain,
Building2,
} from 'lucide-react'
import type { AIControlSettings, CompanyLabel, CategoryInfo } from '@/types/settings'
type TabType = 'profile' | 'accounts' | 'vip' | 'rules' | 'subscription'
type TabType = 'profile' | 'accounts' | 'vip' | 'ai-control' | 'subscription'
interface EmailAccount {
id: string
@@ -39,14 +42,6 @@ interface VIPSender {
name?: string
}
interface SortRule {
id: string
name: string
condition: string
category: string
enabled: boolean
}
interface Subscription {
status: string
plan: string
@@ -55,11 +50,23 @@ interface Subscription {
}
export function Settings() {
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:52',message:'Settings component rendering',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
} catch(e) {}
// #endregion
const { user } = useAuth()
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const activeTab = (searchParams.get('tab') as TabType) || 'profile'
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:60',message:'Settings state initialized',data:{hasUser:!!user,activeTab},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
} catch(e) {}
// #endregion
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
@@ -70,31 +77,79 @@ export function Settings() {
const [connectingProvider, setConnectingProvider] = useState<string | null>(null)
const [vipSenders, setVipSenders] = useState<VIPSender[]>([])
const [newVipEmail, setNewVipEmail] = useState('')
const [rules, setRules] = useState<SortRule[]>([
{ id: '1', name: 'Boss Emails', condition: 'from:boss@company.com', category: 'Important', enabled: true },
{ id: '2', name: 'Support Tickets', condition: 'subject:Ticket #', category: 'Clients', enabled: true },
])
const [subscription, setSubscription] = useState<Subscription | null>(null)
// AI Control state
const [aiControlSettings, setAiControlSettings] = useState<AIControlSettings>({
enabledCategories: [],
categoryActions: {},
autoDetectCompanies: true,
})
const [categories, setCategories] = useState<CategoryInfo[]>([])
const [companyLabels, setCompanyLabels] = useState<CompanyLabel[]>([])
const [newCompanyLabel, setNewCompanyLabel] = useState({ name: '', condition: '', category: 'promotions' })
useEffect(() => {
loadData()
}, [user])
const loadData = async () => {
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:84',message:'loadData called',data:{hasUser:!!user,userId:user?.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
} catch(e) {}
// #endregion
if (!user?.$id) return
setLoading(true)
try {
const [accountsRes, subsRes, prefsRes] = await Promise.all([
const [accountsRes, subsRes, prefsRes, aiControlRes, companyLabelsRes] = await Promise.all([
api.getEmailAccounts(user.$id),
api.getSubscriptionStatus(user.$id),
api.getUserPreferences(user.$id),
api.getAIControlSettings(user.$id),
api.getCompanyLabels(user.$id),
])
if (accountsRes.data) setAccounts(accountsRes.data)
if (subsRes.data) setSubscription(subsRes.data)
if (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders)
if (aiControlRes.data) setAiControlSettings(aiControlRes.data)
if (companyLabelsRes.data) setCompanyLabels(companyLabelsRes.data)
// Load categories from API or use defaults
const categoryList: CategoryInfo[] = [
{ key: 'vip', name: 'Important', description: 'Important emails from known contacts', defaultAction: 'star', color: '#ff0000', enabled: true },
{ key: 'customers', name: 'Clients', description: 'Emails from clients and projects', defaultAction: 'inbox', color: '#4285f4', enabled: true },
{ key: 'invoices', name: 'Invoices', description: 'Invoices, receipts and financial documents', defaultAction: 'inbox', color: '#0f9d58', enabled: true },
{ key: 'newsletters', name: 'Newsletter', description: 'Regular newsletters and updates', defaultAction: 'archive_read', color: '#9c27b0', enabled: true },
{ key: 'promotions', name: 'Promotions', description: 'Marketing emails and promotions', defaultAction: 'archive_read', color: '#ff9800', enabled: true },
{ key: 'social', name: 'Social', description: 'Social media and platform notifications', defaultAction: 'archive_read', color: '#00bcd4', enabled: true },
{ key: 'security', name: 'Security', description: 'Security codes and notifications', defaultAction: 'inbox', color: '#f44336', enabled: true },
{ key: 'calendar', name: 'Calendar', description: 'Calendar invites and events', defaultAction: 'inbox', color: '#673ab7', enabled: true },
{ key: 'review', name: 'Review', description: 'Emails that need manual review', defaultAction: 'inbox', color: '#607d8b', enabled: true },
]
// Update enabled status from settings
const enabledCategories = aiControlRes.data?.enabledCategories || categoryList.map(c => c.key)
const updatedCategories = categoryList.map(cat => ({
...cat,
enabled: enabledCategories.includes(cat.key),
}))
setCategories(updatedCategories)
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:122',message:'loadData success',data:{accountsCount:accountsRes.data?.length||0,categoriesCount:updatedCategories.length},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
} catch(e) {}
// #endregion
} catch (error) {
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:123',message:'loadData error',data:{errorMessage:error instanceof Error?error.message:String(error),errorStack:error instanceof Error?error.stack:undefined},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
} catch(e) {}
// #endregion
console.error('Failed to load settings data:', error)
} finally {
setLoading(false)
@@ -180,12 +235,6 @@ export function Settings() {
}
}
const toggleRule = (ruleId: string) => {
setRules(rules.map(r =>
r.id === ruleId ? { ...r, enabled: !r.enabled } : r
))
}
const handleManageSubscription = async () => {
if (!user?.$id) return
@@ -216,10 +265,16 @@ export function Settings() {
{ id: 'profile' as TabType, label: 'Profile', icon: User },
{ id: 'accounts' as TabType, label: 'Email Accounts', icon: Mail },
{ id: 'vip' as TabType, label: 'VIP List', icon: Star },
{ id: 'rules' as TabType, label: 'Sorting Rules', icon: SettingsIcon },
{ id: 'ai-control' as TabType, label: 'AI Control', icon: Brain },
{ id: 'subscription' as TabType, label: 'Subscription', icon: CreditCard },
]
// #region agent log
try {
fetch('http://127.0.0.1:7242/ingest/4fa7412d-6f79-4871-8728-29c37c9e5772',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Settings.tsx:243',message:'Settings render starting',data:{loading,activeTab,hasUser:!!user},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
} catch(e) {}
// #endregion
return (
<div className="min-h-screen bg-slate-50">
<header className="bg-white border-b border-slate-200 sticky top-0 z-40">
@@ -463,41 +518,335 @@ export function Settings() {
</Card>
)}
{activeTab === 'rules' && (
<Card>
<CardHeader>
<CardTitle>Sorting Rules</CardTitle>
<CardDescription>Custom rules for email sorting</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{rules.map((rule) => (
<div key={rule.id} className={`flex items-center justify-between p-4 rounded-lg border ${
rule.enabled ? 'bg-white border-slate-200' : 'bg-slate-50 border-slate-100'
}`}>
<div className="flex items-center gap-4">
<button
onClick={() => toggleRule(rule.id)}
className={`w-10 h-6 rounded-full transition-colors ${rule.enabled ? 'bg-primary-500' : 'bg-slate-300'}`}
>
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
rule.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<div>
<p className={`font-medium ${rule.enabled ? 'text-slate-900' : 'text-slate-500'}`}>{rule.name}</p>
<p className="text-sm text-slate-500 font-mono">{rule.condition}</p>
</div>
{activeTab === 'ai-control' && (
<div className="space-y-6">
{/* Category Toggles */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Brain className="w-5 h-5 text-primary-500" />
Category Control
</CardTitle>
<CardDescription>Enable or disable email categories for AI sorting</CardDescription>
</div>
<Badge variant={rule.enabled ? 'default' : 'secondary'}>{rule.category}</Badge>
<Button
onClick={async () => {
if (!user?.$id) return
setSaving(true)
try {
await api.saveAIControlSettings(user.$id, {
enabledCategories: aiControlSettings.enabledCategories,
categoryActions: aiControlSettings.categoryActions,
autoDetectCompanies: aiControlSettings.autoDetectCompanies,
})
showMessage('success', 'AI Control settings saved!')
} catch {
showMessage('error', 'Failed to save settings')
} finally {
setSaving(false)
}
}}
disabled={saving}
size="sm"
>
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Save
</Button>
</div>
))}
</CardHeader>
<CardContent className="space-y-4">
{categories.map((category) => (
<div key={category.key} className="flex items-center justify-between p-4 rounded-lg border border-slate-200 bg-white">
<div className="flex items-center gap-4 flex-1">
<button
onClick={() => {
const newEnabled = category.enabled
? aiControlSettings.enabledCategories.filter(c => c !== category.key)
: [...aiControlSettings.enabledCategories, category.key]
setAiControlSettings({ ...aiControlSettings, enabledCategories: newEnabled })
setCategories(categories.map(c => c.key === category.key ? { ...c, enabled: !c.enabled } : c))
}}
className={`w-10 h-6 rounded-full transition-colors ${category.enabled ? 'bg-primary-500' : 'bg-slate-300'}`}
>
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
category.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded" style={{ backgroundColor: category.color }} />
<p className={`font-medium ${category.enabled ? 'text-slate-900' : 'text-slate-500'}`}>
{category.name}
</p>
</div>
<p className="text-sm text-slate-500">{category.description}</p>
</div>
</div>
{category.enabled && (
<select
value={aiControlSettings.categoryActions[category.key] || category.defaultAction}
onChange={(e) => {
const newActions = { ...aiControlSettings.categoryActions, [category.key]: e.target.value as any }
setAiControlSettings({ ...aiControlSettings, categoryActions: newActions })
}}
className="px-3 py-1.5 text-sm border border-slate-300 rounded-lg bg-white"
>
<option value="inbox">Keep in Inbox</option>
<option value="archive_read">Archive & Mark Read</option>
<option value="star">Star</option>
</select>
)}
</div>
))}
</CardContent>
</Card>
<Button variant="outline" className="w-full">
<Plus className="w-4 h-4 mr-2" />
Create new rule
</Button>
</CardContent>
</Card>
{/* Company Labels */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-primary-500" />
Company Labels
</CardTitle>
<CardDescription>Automatically label emails from specific companies</CardDescription>
</div>
<Button
onClick={async () => {
if (!user?.$id) return
setSaving(true)
try {
await api.saveAIControlSettings(user.$id, {
autoDetectCompanies: aiControlSettings.autoDetectCompanies,
})
showMessage('success', 'Settings saved!')
} catch {
showMessage('error', 'Failed to save')
} finally {
setSaving(false)
}
}}
disabled={saving}
size="sm"
>
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Save
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Auto-Detection Toggle */}
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 bg-white">
<div>
<p className="font-medium text-slate-900">Auto-Detect Known Companies</p>
<p className="text-sm text-slate-500">Automatically detect and label emails from Amazon, Google, Microsoft, etc. Labels are created automatically when emails from these companies are detected.</p>
</div>
<button
onClick={() => {
const newValue = !aiControlSettings.autoDetectCompanies
setAiControlSettings({ ...aiControlSettings, autoDetectCompanies: newValue })
}}
className={`w-10 h-6 rounded-full transition-colors ${aiControlSettings.autoDetectCompanies ? 'bg-primary-500' : 'bg-slate-300'}`}
>
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
aiControlSettings.autoDetectCompanies ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
</div>
{/* Custom Company Labels - Optional */}
{companyLabels.length > 0 && (
<div className="space-y-4">
<h3 className="font-semibold text-slate-900">Custom Company Labels (Optional)</h3>
{/* Add New Label Form */}
<div className="p-4 rounded-lg border border-slate-200 bg-slate-50 space-y-3">
<div>
<Label htmlFor="label-name">Company Name</Label>
<Input
id="label-name"
placeholder="e.g., Amazon"
value={newCompanyLabel.name}
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, name: e.target.value })}
/>
</div>
<div>
<Label htmlFor="label-condition">Condition</Label>
<Input
id="label-condition"
placeholder="e.g., from:amazon.com OR from:amazon.de"
value={newCompanyLabel.condition}
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, condition: e.target.value })}
/>
<p className="text-xs text-slate-500 mt-1">Use "from:domain.com" or "subject:keyword"</p>
</div>
<div>
<Label htmlFor="label-category">Category</Label>
<select
id="label-category"
value={newCompanyLabel.category}
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, category: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg bg-white"
>
<option value="promotions">Promotions</option>
<option value="newsletters">Newsletter</option>
<option value="invoices">Invoices</option>
<option value="customers">Clients</option>
</select>
</div>
<Button
onClick={async () => {
if (!user?.$id || !newCompanyLabel.name || !newCompanyLabel.condition) return
try {
const saved = await api.saveCompanyLabel(user.$id, {
...newCompanyLabel,
enabled: true,
})
if (saved.data) {
setCompanyLabels([...companyLabels, saved.data])
setNewCompanyLabel({ name: '', condition: '', category: 'promotions' })
showMessage('success', 'Company label created!')
}
} catch {
showMessage('error', 'Failed to create label')
}
}}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
Add Company Label
</Button>
</div>
{/* Existing Labels */}
<div className="space-y-2">
{companyLabels.map((label) => (
<div key={label.id} className="flex items-center justify-between p-4 rounded-lg border border-slate-200 bg-white">
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="font-medium text-slate-900">{label.name}</p>
<Badge variant={label.enabled ? 'default' : 'secondary'}>
{label.enabled ? 'Enabled' : 'Disabled'}
</Badge>
</div>
<p className="text-sm text-slate-500 font-mono mt-1">{label.condition}</p>
{label.category && (
<p className="text-xs text-slate-400 mt-1">Category: {label.category}</p>
)}
</div>
<div className="flex items-center gap-2">
<button
onClick={async () => {
if (!user?.$id || !label.id) return
try {
await api.saveCompanyLabel(user.$id, { ...label, enabled: !label.enabled })
setCompanyLabels(companyLabels.map(l => l.id === label.id ? { ...l, enabled: !l.enabled } : l))
showMessage('success', 'Label updated!')
} catch {
showMessage('error', 'Failed to update label')
}
}}
className={`w-10 h-6 rounded-full transition-colors ${label.enabled ? 'bg-primary-500' : 'bg-slate-300'}`}
>
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
label.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<Button
variant="ghost"
size="icon"
onClick={async () => {
if (!user?.$id || !label.id) return
try {
await api.deleteCompanyLabel(user.$id, label.id)
setCompanyLabels(companyLabels.filter(l => l.id !== label.id))
showMessage('success', 'Label deleted!')
} catch {
showMessage('error', 'Failed to delete label')
}
}}
>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
</div>
))}
</div>
</div>
)}
{/* Add Custom Label - Collapsible */}
<details className="group">
<summary className="cursor-pointer p-4 rounded-lg border border-slate-200 bg-slate-50 hover:bg-slate-100 transition-colors">
<div className="flex items-center justify-between">
<span className="font-medium text-slate-900">Add Custom Company Label (Optional)</span>
<Plus className="w-5 h-5 text-slate-500 group-open:rotate-45 transition-transform" />
</div>
</summary>
<div className="mt-4 p-4 rounded-lg border border-slate-200 bg-white space-y-3">
<div>
<Label htmlFor="label-name">Company Name</Label>
<Input
id="label-name"
placeholder="e.g., Amazon"
value={newCompanyLabel.name}
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, name: e.target.value })}
/>
</div>
<div>
<Label htmlFor="label-condition">Condition</Label>
<Input
id="label-condition"
placeholder="e.g., from:amazon.com OR from:amazon.de"
value={newCompanyLabel.condition}
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, condition: e.target.value })}
/>
<p className="text-xs text-slate-500 mt-1">Use "from:domain.com" or "subject:keyword"</p>
</div>
<div>
<Label htmlFor="label-category">Category</Label>
<select
id="label-category"
value={newCompanyLabel.category}
onChange={(e) => setNewCompanyLabel({ ...newCompanyLabel, category: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg bg-white"
>
<option value="promotions">Promotions</option>
<option value="newsletters">Newsletter</option>
<option value="invoices">Invoices</option>
<option value="customers">Clients</option>
</select>
</div>
<Button
onClick={async () => {
if (!user?.$id || !newCompanyLabel.name || !newCompanyLabel.condition) return
try {
const saved = await api.saveCompanyLabel(user.$id, {
...newCompanyLabel,
enabled: true,
})
if (saved.data) {
setCompanyLabels([...companyLabels, saved.data])
setNewCompanyLabel({ name: '', condition: '', category: 'promotions' })
showMessage('success', 'Company label created!')
}
} catch {
showMessage('error', 'Failed to create label')
}
}}
className="w-full"
>
<Plus className="w-4 h-4 mr-2" />
Add Company Label
</Button>
</div>
</details>
</CardContent>
</Card>
</div>
)}
{activeTab === 'subscription' && (

View File

@@ -0,0 +1,32 @@
/**
* TypeScript types for Settings and AI Control
*/
export interface AIControlSettings {
enabledCategories: string[]
categoryActions: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies: boolean
}
export interface CompanyLabel {
id?: string
name: string
condition: string
enabled: boolean
category?: string
}
export interface CategoryInfo {
key: string
name: string
description: string
defaultAction: 'inbox' | 'archive_read' | 'star'
color: string
enabled: boolean
}
export interface KnownCompany {
name: string
domain: string
enabled: boolean
}