// KI-Assistent — provider-agnostic AI assistant with insights feed + chat const AI_PROVIDERS = [ { id: "claude", name: "Claude", vendor: "Anthropic", models: ["claude-sonnet-4.5", "claude-haiku-4.5"], color: "#cc785c" }, { id: "openai", name: "GPT", vendor: "OpenAI", models: ["gpt-4o", "gpt-4o-mini"], color: "#10a37f" }, { id: "gemini", name: "Gemini", vendor: "Google", models: ["gemini-2.5-pro", "gemini-2.5-flash"], color: "#4285f4" }, { id: "mistral", name: "Mistral", vendor: "Mistral AI", models: ["mistral-large", "mistral-small"], color: "#ff7000" }, { id: "ollama", name: "Ollama", vendor: "Lokal", models: ["llama3.1:70b", "qwen2.5:32b"], color: "#888888" }, ]; // Curated demo insights — these read like real findings an LLM would surface const AI_INSIGHTS = [ { id: "i1", category: "Sparpotenzial", priority: "hoch", date: "2026-04-29", title: "Heizungswartungs­kosten 38 % über Markt­durchschnitt", summary: "Heizung Berger berechnet 240 € pro Wartung in der Lindenstraße. Der regionale Durchschnitt liegt bei 145–180 €. Empfehlung: 2 Vergleichs­angebote einholen.", saving: 720, // EUR/year action: "Vergleichsangebote anfordern", refs: [{ kind: "contact", id: "c3", label: "Heizung Berger" }, { kind: "property", id: "p1", label: "Lindenstraße 42" }], confidence: 0.86, }, { id: "i2", category: "Steuer", priority: "mittel", date: "2026-04-28", title: "AfA Lindenstraße: 2 % statt 2,5 % anwendbar", summary: "Da das Gebäude vor 1925 errichtet wurde (Baujahr 1908), kannst du die erhöhte lineare AfA von 2,5 % geltend machen. Aktuell sind 2 % angesetzt — zusätzliche Werbungskosten ca. 7.250 €/Jahr.", saving: 2900, action: "Mit Steuerberater klären", refs: [{ kind: "property", id: "p1", label: "Lindenstraße 42" }, { kind: "contact", id: "c6", label: "Kanzlei Wiegand" }], confidence: 0.78, }, { id: "i3", category: "Beleg", priority: "hoch", date: "2026-04-22", title: "Beleg fehlt: Bad-Renovierung 3.200 €", summary: "Anzahlung an Klempnerei Heinrich am 19.04. ist als Buchung erfasst, aber kein Rechnungs­dokument hinterlegt. Für den steuerlichen Nachweis und §35a EStG benötigst du die Originalrechnung.", action: "Rechnung anfordern", refs: [{ kind: "contact", id: "c1", label: "Klempnerei Heinrich" }, { kind: "property", id: "p1", label: "Lindenstraße 42" }], confidence: 0.99, }, { id: "i4", category: "Recht", priority: "hoch", date: "2026-04-20", title: "Mietspiegel: Wohnung 4 unter Marktmiete", summary: "Lindenstraße 42, Wohnung 4 (84 m², 1. OG) wird voraussichtlich für 1.080 € neu vermietet. Der Berliner Mietspiegel 2025 erlaubt für vergleichbare Lagen bis zu 14,80 €/m² Kaltmiete (≈ 1.243 €). Mieterhöhung möglich, aber Kappungsgrenze beachten.", saving: 1956, action: "Mietangebot anpassen", refs: [{ kind: "property", id: "p1", label: "Lindenstraße 42" }, { kind: "unit", id: "u4", label: "Wohnung 4" }], confidence: 0.82, }, { id: "i5", category: "Anomalie", priority: "mittel", date: "2026-04-15", title: "Stromverbrauch Wohnung 1 unplausibel niedrig", summary: "Letzte Ablesung 4.821 kWh — das entspricht ca. 2.100 kWh/Jahr für eine 2,5-Zimmer-Wohnung. Erwartungs­bereich: 2.800–3.500 kWh. Möglich: defekter Zähler, Untermietung oder Übermittlungsfehler.", action: "Zähler prüfen lassen", refs: [{ kind: "unit", id: "u1", label: "Wohnung 1" }], confidence: 0.71, }, { id: "i6", category: "Frist", priority: "hoch", date: "2026-04-12", title: "Nebenkosten­abrechnung 2024 — letzte 30 Tage", summary: "§ 556 Abs. 3 BGB: Die Abrechnung muss spätestens 12 Monate nach Ende des Abrechnungs­zeitraums dem Mieter vorliegen. Für Stadtpark 7 (3 Mieter) endet die Frist am 31.05.2026.", action: "Abrechnung erstellen", refs: [{ kind: "property", id: "p2", label: "Am Stadtpark 7" }], confidence: 0.95, }, { id: "i7", category: "Sparpotenzial", priority: "niedrig", date: "2026-04-08", title: "Versicherungs­prämie Eichenallee bündelbar", summary: "Die Gebäude­versicherung bei Allianz für Eichenallee fehlt — alle anderen 3 Objekte sind bei Allianz versichert. Sammelvertrag erfahrungs­gemäß 8–12 % günstiger.", saving: 280, action: "Sammelangebot einholen", refs: [{ kind: "property", id: "p4", label: "Eichenallee 3" }, { kind: "contact", id: "c7", label: "Allianz" }], confidence: 0.74, }, ]; const TOTAL_SAVINGS = AI_INSIGHTS.reduce((s, i) => s + (i.saving || 0), 0); window.AI_PROVIDERS = AI_PROVIDERS; window.AI_INSIGHTS = AI_INSIGHTS; window.AI_TOTAL_SAVINGS = TOTAL_SAVINGS; // ─────────────────────────────────────────────────────────────────── function AssistantPage({ data, setRoute }) { const [tab, setTab] = React.useState('insights'); const [health, setHealth] = React.useState(null); const [insights, setInsights] = React.useState({ loading: true, items: [], generatedAt: null, error: null }); const reloadHealth = React.useCallback(async () => { try { setHealth(await window.getHealth()); } catch { setHealth({ ok: false, ocr: { enabled: false } }); } }, []); const reloadInsights = React.useCallback(async (force) => { setInsights(s => ({ ...s, loading: true, error: null })); try { const r = await window.getAiInsights(force); setInsights({ loading: false, items: r.insights || [], generatedAt: r.generatedAt, cached: r.cached, error: null }); } catch (e) { setInsights({ loading: false, items: [], generatedAt: null, error: e.detail || e.message }); } }, []); React.useEffect(() => { reloadHealth(); reloadInsights(false); }, [reloadHealth, reloadInsights]); const aiEnabled = !!health?.ocr?.enabled; const model = health?.ocr?.model || ''; return (
KI-Assistent {aiEnabled ? Verbunden · OpenAI {model} : OPENAI_API_KEY nicht gesetzt}

Berater

Analysiert deine Daten, findet Sparpotenziale, beantwortet Fragen.
{tab === 'insights' && } {tab === 'chat' && } {tab === 'audit' && }
); } // ───── Insights tab ───── function InsightsTab({ data, setRoute, insights, aiEnabled, reload }) { const [filter, setFilter] = React.useState('alle'); const cats = ['alle', 'Sparpotenzial', 'Steuer', 'Recht', 'Beleg', 'Frist', 'Anomalie', 'Auslastung']; const items = insights.items || []; const filtered = filter === 'alle' ? items : items.filter(i => i.category === filter); const totalSavings = items.reduce((s, i) => s + (i.savingsEur || 0), 0); const highPrioCount = items.filter(i => i.priority === 'hoch').length; const lastAnalysis = insights.generatedAt ? new Date(insights.generatedAt) : null; const ago = (() => { if (!lastAnalysis) return '—'; const mins = Math.floor((Date.now() - lastAnalysis.getTime()) / 60000); if (mins < 1) return 'gerade eben'; if (mins < 60) return `vor ${mins} Min`; const h = Math.floor(mins / 60); if (h < 24) return `vor ${h} Std`; return lastAnalysis.toLocaleDateString('de-DE'); })(); if (!aiEnabled) { return (
🔌
KI nicht verbunden
Setze OPENAI_API_KEY in der .env auf dem Server, dann analysiert die KI dein Portfolio automatisch.
); } if (insights.error) { return (
KI-Analyse fehlgeschlagen
{insights.error}
); } if (insights.loading && items.length === 0) { return (
KI analysiert dein Portfolio…
Dauert ~5–15 Sek beim ersten Aufruf, danach 24h gecached.
); } if (items.length === 0) { return (
Keine Hinweise gefunden
Entweder ist alles im grünen Bereich, oder es gibt zu wenige Daten für valide Insights. Lege mehr Buchungen / Mieter / Objekte an oder klick „Neue Analyse".
); } return (
Sparpotenzial p.a.
{fmtEUR(totalSavings)}
{totalSavings > 0 ? 'aus quantifizierbaren Hinweisen' : '—'}
Offene Hinweise
{items.length}
{highPrioCount > 0 ? `${highPrioCount} mit hoher Priorität` : 'keine kritischen'}
Letzte Analyse
{ago}
{insights.cached ? 'aus Cache' : 'frisch generiert'}
Modell
OpenAI
analysiert deine echten Daten
{cats.map(c => { const count = c === 'alle' ? items.length : items.filter(i => i.category === c).length; if (c !== 'alle' && count === 0) return null; return ( ); })}
{filtered.map((ins, i) => )}
); } function InsightCard({ ins, setRoute, data }) { const [creating, setCreating] = React.useState(false); const [created, setCreated] = React.useState(null); // { taskId } when done const [err, setErr] = React.useState(null); const createTask = async () => { setCreating(true); setErr(null); try { const dueIn14 = new Date(); dueIn14.setDate(dueIn14.getDate() + 14); const dueDate = dueIn14.toISOString().slice(0, 10); const desc = `[KI-Berater · ${ins.category}] ${ins.description}` + (typeof ins.savingsEur === 'number' && ins.savingsEur > 0 ? `\n\n💰 Sparpotenzial: ${fmtEUR(ins.savingsEur)} p.a.` : ''); const t = await data.store.addTask({ title: ins.actionLabel ? `${ins.actionLabel}: ${ins.title}` : ins.title, description: desc, propertyId: ins.propertyId || null, unitId: ins.unitId || null, priority: ins.priority || 'mittel', dueDate, }); setCreated(t); } catch (e) { setErr(e.message); } finally { setCreating(false); } }; const refs = []; if (ins.propertyId) { const p = data.properties.find(x => x.id === ins.propertyId); if (p) refs.push({ kind: 'property', id: p.id, label: p.name }); } if (ins.unitId) { const prop = data.properties.find(p => p.units.some(u => u.id === ins.unitId)); const u = prop?.units.find(u => u.id === ins.unitId); if (u && prop) refs.push({ kind: 'unit', id: u.id, propertyId: prop.id, label: `${prop.name} · ${u.label}` }); } if (ins.tenantId) { const t = data.tenants?.[ins.tenantId]; if (t) refs.push({ kind: 'tenant', id: t.id, label: t.name }); } return (
{ins.category} · Priorität {ins.priority} · {Math.round((ins.confidence || 0) * 100)} % Konfidenz
{ins.title}
{ins.description}
{refs.length > 0 && (
{refs.map((r, i) => ( ))}
)}
{typeof ins.savingsEur === 'number' && ins.savingsEur > 0 && (
Ersparnis p.a.
{fmtEUR(ins.savingsEur)}
)} {ins.actionLabel && (
{created ? (
✓ Aufgabe angelegt
) : ( <> {err && ⚠ {err}} )}
)}
); } // ───── Chat tab ───── function ChatTab({ data, aiEnabled, model }) { const [messages, setMessages] = React.useState([ { role: 'assistant', content: `Hallo! Ich bin dein KI-Berater. Ich kenne dein Portfolio (Objekte, Mieter, Buchungen) und kann Fragen zu **Steuerrecht**, **Mietrecht** und **Bewirtschaftung** beantworten — basierend auf deinen echten Daten.` }, ]); const [input, setInput] = React.useState(''); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const streamRef = React.useRef(null); const suggestions = [ "Welche meiner Ausgaben sind auffällig hoch?", "Welche Mieterhöhungen wären 2026 zulässig?", "Welche Belege fehlen für die Steuererklärung?", "Wie ist die Auslastung meines Portfolios?", ]; React.useEffect(() => { if (streamRef.current) streamRef.current.scrollTop = streamRef.current.scrollHeight; }, [messages, loading]); const send = async (text) => { if (!text.trim() || !aiEnabled) return; const userMsg = { role: 'user', content: text }; const newHistory = [...messages, userMsg]; setMessages(newHistory); setInput(''); setLoading(true); setError(null); try { const r = await window.aiChat(text, messages.slice(1)); // skip seed greeting setMessages(m => [...m, { role: 'assistant', content: r.reply }]); } catch (e) { setError(e.detail || e.message); } finally { setLoading(false); } }; if (!aiEnabled) { return (
🔌
Chat nicht verfügbar
Setze OPENAI_API_KEY in der .env, dann startet der Chat.
); } return (
{messages.map((m, i) => (
{m.role === 'assistant' && (
)}
))} {loading && (
)} {error && (
!
⚠ {error}
)}
{messages.length <= 1 && (
{suggestions.map((s, i) => ( ))}
)}
setInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && !loading) send(input); }} disabled={loading} />
Dein Portfolio wird automatisch als Kontext mitgesendet · OpenAI {model}
); } function formatMarkdown(text) { return text .replace(/&/g, '&').replace(//g, '>') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\n\n/g, '

') .replace(/^(.+)$/, '

$1

') .replace(/•/g, '·'); } // ───── Audit (Belegprüfung) tab ───── const AUDIT_BELEGE = [ { id: 1, file: "Rechnung_Klempner_2026-04.pdf", contact: "Klempnerei Heinrich", amount: 380, date: "2026-04-12", propertyId: "p1", unitId: "u3", scope: "einheit", category: "Reparatur", status: "ok", findings: [], tips: ["§35a EStG: Lohnanteil (geschätzt 220 €) als haushaltsnahe Dienstleistung steuerlich ansetzbar."] }, { id: 2, file: "Heizungswartung_Berger.pdf", contact: "Heizung Berger", amount: 240, date: "2026-04-04", propertyId: "p1", unitId: null, scope: "allgemein", category: "Wartung", status: "warn", findings: ["Stundensatz 38 % über Markt", "Kein Wartungsbericht angehängt"], tips: ["Wartungskosten sind als Allgemeinkosten umlagefähig (BetrKV §2 Nr. 4a).", "Vergleichsangebot anfordern — Einsparpotenzial ~95 €."] }, { id: 3, file: "Versicherung_Allianz_2026.pdf", contact: "Allianz", amount: 1240, date: "2026-01-15", propertyId: "p1", unitId: null, scope: "allgemein", category: "Versicherung", status: "ok", findings: [], tips: ["Wohngebäudeversicherung voll als Werbungskosten absetzbar.", "Anteilig auf Mieter umlegbar."] }, { id: 4, file: "Steuerberater_Q1.pdf", contact: "Kanzlei Wiegand", amount: 480, date: "2026-03-18", propertyId: "p1", unitId: null, scope: "allgemein", category: "Beratung", status: "ok", findings: [], tips: ["100 % als Werbungskosten absetzbar."] }, { id: 5, file: "Schornsteinfeger_2026-02.pdf", contact: "Schornsteinfeger Maier", amount: 95, date: "2026-02-09", propertyId: "p2", unitId: null, scope: "allgemein", category: "Wartung", status: "ok", findings: [], tips: ["Voll umlagefähig auf Mieter."] }, { id: 6, file: "Fenstertausch_W3.pdf", contact: "Elektro Voss", amount: 2400, date: "2026-03-14", propertyId: "p1", unitId: "u3", scope: "einheit", category: "Modernisierung", status: "warn", findings: ["Lieferant ist Elektriker — Gewerk passt nicht zur Leistung"], tips: ["Modernisierung > 4.000 €: Über AfA verteilen, nicht sofort absetzen.", "Bis zu 8 % auf Miete umlegbar (§559 BGB)."] }, { id: 7, file: "Hausmeister_April.pdf", contact: "Service Klar", amount: 180, date: "2026-04-30", propertyId: "p2", unitId: null, scope: "allgemein", category: "Hausmeister", status: "ok", findings: [], tips: ["Lohnkostenanteil (~70 %) für §35a EStG ansetzen."] }, { id: 8, file: "Bad_Renovierung_Anzahlung.pdf", contact: "Klempnerei Heinrich", amount: 3200, date: "2026-04-19", propertyId: "p1", unitId: "u3", scope: "einheit", category: "Renovierung", status: "missing", findings: ["Originalrechnung nicht hochgeladen"], tips: ["Ohne Originalbeleg keine §35a-Anrechnung möglich.", "Rechnung muss Lohn- und Materialkosten getrennt ausweisen."] }, { id: 9, file: "Strom_Allgemein_Q1.pdf", contact: "Stadtwerke Berlin", amount: 145, date: "2026-04-02", propertyId: "p1", unitId: null, scope: "allgemein", category: "Strom", status: "ok", findings: [], tips: ["Allgemeinstrom (Treppenhaus, Keller) auf Mieter umlegbar."] }, { id: 10, file: "Gartenarbeit_April.pdf", contact: "Grünwerk GmbH", amount: 320, date: "2026-04-22", propertyId: "p4", unitId: null, scope: "allgemein", category: "Gartenpflege", status: "warn", findings: ["Rechnung weist Lohn- und Materialkosten nicht getrennt aus"], tips: ["Trennung anfordern — sonst kein §35a EStG-Abzug möglich."] }, ]; const AUDIT_GLOBAL_TIPS = [ { tone: "pos", title: "92 % der Belege vollständig", body: "Sehr gute Quote. Reichst du den Steuerberater-Ordner zum Quartalsende ein, sparst du dir die hektische Sammlung im März." }, { tone: "warn", title: "2 Belege ohne Lohn-/Materialtrennung", body: "Bei haushaltsnahen Dienstleistungen müssen Rechnungen den Lohnanteil separat ausweisen — sonst gibt es keinen §35a-Steuerbonus." }, { tone: "info", title: "Originalbelege scannen reicht", body: "Seit der GoBD-Novelle 2020 sind digitale Belege gleichwertig, solange sie unverändert archiviert werden. Hausgut erfüllt das automatisch." }, { tone: "neg", title: "1 fehlender Beleg ab 2.000 €", body: "Anzahlung Bad-Renovierung 3.200 € — fordere die Originalrechnung umgehend an. Anzahlungsbelege gelten beim Finanzamt nur als Zahlungsbeleg." }, ]; function AuditTab({ data }) { const [filter, setFilter] = React.useState({ status: 'all', property: 'all', scope: 'all' }); const [view, setView] = React.useState('belege'); // belege | mappen | tipps const cloudBelege = useDocuments({}); const belegeInputRef = React.useRef(null); const [pendingFiles, setPendingFiles] = React.useState(null); const onBelegFiles = (e) => { const files = Array.from(e.target.files || []).filter(Boolean); if (files.length) setPendingFiles(files); if (belegeInputRef.current) belegeInputRef.current.value = ''; }; const filtered = AUDIT_BELEGE.filter(a => { if (filter.status !== 'all' && a.status !== filter.status) return false; if (filter.property !== 'all' && a.propertyId !== filter.property) return false; if (filter.scope !== 'all' && a.scope !== filter.scope) return false; return true; }); const byProp = {}; AUDIT_BELEGE.forEach(b => { if (!byProp[b.propertyId]) byProp[b.propertyId] = { allgemein: 0, einheit: 0, count: 0, missing: 0, warn: 0 }; byProp[b.propertyId][b.scope] += b.amount; byProp[b.propertyId].count++; if (b.status === 'missing') byProp[b.propertyId].missing++; if (b.status === 'warn') byProp[b.propertyId].warn++; }); const totalOk = AUDIT_BELEGE.filter(a => a.status === 'ok').length; const totalWarn = AUDIT_BELEGE.filter(a => a.status === 'warn').length; const totalMiss = AUDIT_BELEGE.filter(a => a.status === 'missing').length; return (
{/* Sub-views */}
{view === 'belege' && ( <> {/* Stats + Upload */}
Geprüft
{totalOk}
vollständig
Auffällig
{totalWarn}
prüfen
Fehlend
{totalMiss}
Beleg anfordern
Allgemeinkosten YTD
{fmtEUR(AUDIT_BELEGE.filter(b => b.scope === 'allgemein').reduce((s, b) => s + b.amount, 0))}
umlagefähig
Einheits-Kosten YTD
{fmtEUR(AUDIT_BELEGE.filter(b => b.scope === 'einheit').reduce((s, b) => s + b.amount, 0))}
direkt zuordenbar
{pendingFiles && ( { setPendingFiles(null); await cloudBelege.reload(); }} onCancel={() => setPendingFiles(null)} /> )}
{/* Filter bar */}
setFilter({ ...filter, status: v })} /> setFilter({ ...filter, scope: v })} /> ({ v: p.id, l: p.name })) ]} onChange={v => setFilter({ ...filter, property: v })} />
{filtered.map(a => { const prop = data.properties.find(p => p.id === a.propertyId); const unit = prop?.units.find(u => u.id === a.unitId); return (
{a.status === 'ok' && } {a.status === 'warn' && '!'} {a.status === 'missing' && '?'}
{a.file}
{a.contact}· {fmtDate(a.date)}· {fmtEUR(a.amount)}· {a.category} {a.scope === 'allgemein' ? 'Allgemein' : 'Einheit'} → {prop?.name}{unit ? ` · ${unit.label}` : ''}
{a.findings.length > 0 && (
    {a.findings.map((f, i) =>
  • {f}
  • )}
)} {a.tips.length > 0 && (
💡
{a.tips.map((tip, i) =>
{tip}
)}
)}
); })}
)} {view === 'mappen' && } {view === 'tipps' && }
); } function FilterGroup({ label, value, options, onChange }) { return (
{label}
{options.map(o => ( ))}
); } function WorkbookView({ data, byProp }) { const months = ['Januar', 'Februar', 'März', 'April']; const [selectedMonth, setSelectedMonth] = React.useState('April'); return (

Steuerberater-Mappen 2026

Pro Hauptobjekt und Monat eine Sammelmappe. Enthält alle Belege, Buchungsliste, Mietnachweise und Allgemein-/Einheits-Aufteilung — fertig zum Versand an deinen Steuerberater.

{months.map(m => ( ))}
{data.properties.map(p => { const stats = byProp[p.id] || { allgemein: 0, einheit: 0, count: 0, missing: 0, warn: 0 }; const total = stats.allgemein + stats.einheit; const allgPct = total ? (stats.allgemein / total) * 100 : 0; const ready = stats.missing === 0; return (
{selectedMonth} 2026
{p.name}
{p.address}
{ready ? '✓ versandbereit' : `${stats.missing} Beleg${stats.missing !== 1 ? 'e' : ''} fehlt`}
u.status === 'vermietet').length} /> {stats.warn > 0 && }
Allgemein {fmtEUR(stats.allgemein)} Einheit {fmtEUR(stats.einheit)}
); })}
Jahresende-Paket
Gesamtmappe 2025
Komplettes Vorjahr: alle 12 Monate, je Hauptobjekt zusammengefasst, mit AfA-Berechnung, Anlage V-Vorbereitung und §35a-Aufstellung.
); } function WorkbookContent({ label, count, warn }) { return (
{warn ? '!' : '✓'} {label} {count}
); } function TipsView() { return (
{AUDIT_GLOBAL_TIPS.map((t, i) => (
{t.tone === 'pos' && '✓'} {t.tone === 'warn' && '!'} {t.tone === 'info' && 'i'} {t.tone === 'neg' && '×'}
{t.title}
{t.body}
))}

Steuer-Spickzettel für Vermieter

); } function CheatItem({ title, body }) { return (
{title}
{body}
); } // ───── Settings tab ───── function SettingsTab({ provider, setProvider, connected, setConnected }) { const [apiKey, setApiKey] = React.useState(''); const [showKey, setShowKey] = React.useState(false); return (

Anbieter

{AI_PROVIDERS.map(p => ( ))}

Verbindung

setApiKey(e.target.value)} disabled={provider === 'ollama'} />
Wird verschlüsselt im Browser gespeichert. Nichts wird an Hausgut-Server übertragen — Anfragen gehen direkt an {AI_PROVIDERS.find(p => p.id === provider).vendor}.
{connected && }

Welche Daten bekommt die KI?

Automatik

Datenschutz

Persönlich identifizierbare Daten (Mieternamen, Adressen, IBANs) werden vor der Übermittlung lokal pseudonymisiert. Du kannst pro Anfrage entscheiden, ob die KI auf alle Objekte oder nur auf eines zugreift.
); } function SettingsToggle({ label, defaultOn = false }) { const [on, setOn] = React.useState(defaultOn); return (
{label}
); } const AIIcon = { Sparkle: () => , Check: () => , Shield: () => , }; Object.assign(window, { AssistantPage, AIIcon });