import React, { useEffect, useRef, useState } from "react"; import { Alert, Box, Button, FormControlLabel, Skeleton, Switch, Typography } from "@mui/material"; import { PageSection } from "../layout/PageLayout"; import { api } from "../api"; import { useI18n } from "../i18n/I18nProvider"; import { useToast } from "../toast"; import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary"; type AiSettings = { enabled: boolean; externalProcessingAllowed: boolean; externalProcessingAvailable: boolean; effectiveExternalProcessing: boolean; provider: string; }; export default function AiPrivacySettingsCard() { const { t } = useI18n(); const { toast } = useToast(); const [settings, setSettings] = useState(null); const [savedSettings, setSavedSettings] = useState(null); const [failed, setFailed] = useState(false); const [saving, setSaving] = useState(false); const [readAttempt, setReadAttempt] = useState(0); const [loading, setLoading] = useState(true); const session = useRef({ mounted: true, generation: 0, saving: false }); const currentTranslation = useRef(t); const body = useRef(null); const retryFocus = useRef(null); const dirty = Boolean(settings && savedSettings && (settings.enabled !== savedSettings.enabled || settings.externalProcessingAllowed !== savedSettings.externalProcessingAllowed)); useEffect(() => { currentTranslation.current = t; }, [t]); useEffect(() => { if (!loading && settings && retryFocus.current) { if (document.activeElement === document.body || document.activeElement === retryFocus.current) body.current?.focus(); retryFocus.current = null; } }, [loading, settings]); useEffect(() => { const owner = session.current; owner.mounted = true; const generation = ++owner.generation; const context = getSessionContext(); const active = () => owner.mounted && owner.generation === generation && !isSessionInvalidated() && getSessionContext() === context; api.get("/ai/settings") .then((response) => { if (active()) { setSettings(response.data); setSavedSettings(response.data); } }) .catch(() => { if (active()) setFailed(true); }) .finally(() => { if (active()) setLoading(false); }); return () => { owner.mounted = false; owner.generation += 1; }; }, [readAttempt]); const save = async () => { checkSessionCookie(); const owner = session.current; if (!settings || !owner.mounted || owner.saving || isSessionInvalidated()) return; const submitted = settings; const context = getSessionContext(); const generation = owner.generation; const active = () => owner.mounted && owner.generation === generation && !isSessionInvalidated() && getSessionContext() === context; owner.saving = true; if (body.current?.contains(document.activeElement)) body.current.focus(); setSaving(true); setFailed(false); try { const response = await api.put("/ai/settings", { enabled: settings.enabled, externalProcessingAllowed: settings.externalProcessingAllowed, }); if (!active()) return; setSavedSettings(response.data); setSettings(current => current === submitted || !current ? response.data : { ...response.data, enabled: current.enabled, externalProcessingAllowed: current.externalProcessingAllowed }); toast(currentTranslation.current("settingsAiSaved"), "success"); } catch { if (active()) setFailed(true); } finally { if (active()) { owner.saving = false; setSaving(false); } } }; return ( {failed ? { retryFocus.current = document.activeElement as HTMLElement; setFailed(false); setLoading(true); setReadAttempt(value => value + 1); }}>{t("retry")} : undefined}>{t(settings ? "settingsAiSaveFailed" : "settingsAiPrivacyUnavailable")} : null} {!settings ? loading ? : null : setSettings({ ...settings, enabled: event.target.checked })} />} label={t("settingsAiEnabled")} /> setSettings({ ...settings, externalProcessingAllowed: event.target.checked })} />} label={t("settingsAiExternalAllowed")} /> {settings.externalProcessingAvailable ? t("settingsAiExternalAvailable") : t("settingsAiExternalLocalOnly")} {dirty ? {t("assetsUnsavedChanges")} : null} {dirty ? : null} } ); }