feat(ui): simplify A3 workspace and refresh saved AI availability

This commit is contained in:
cesnimda
2026-11-04 00:21:04 +01:00
parent 7aa54bff1f
commit 2218659ecf
216 changed files with 7935 additions and 115 deletions
@@ -14,7 +14,7 @@ type AiSettings = {
provider: string;
};
export default function AiPrivacySettingsCard() {
export default function AiPrivacySettingsCard({ onSaved }: { onSaved?: () => Promise<void> } = {}) {
const { t } = useI18n();
const { toast } = useToast();
const [settings, setSettings] = useState<AiSettings | null>(null);
@@ -70,6 +70,10 @@ export default function AiPrivacySettingsCard() {
setSavedSettings(response.data);
setSettings(current => current === submitted || !current ? response.data : { ...response.data, enabled: current.enabled, externalProcessingAllowed: current.externalProcessingAllowed });
toast(currentTranslation.current("settingsAiSaved"), "success");
if (onSaved) {
try { await onSaved(); }
catch { if (active()) toast(currentTranslation.current("settingsAiRefreshFailed"), "warning"); }
}
} catch {
if (active()) setFailed(true);
} finally {
@@ -78,7 +82,7 @@ export default function AiPrivacySettingsCard() {
};
return (
<PageSection surface title={t("settingsAiPrivacyTitle")} description={t("settingsAiPrivacyBody")}>
<PageSection title={t("settingsAiPrivacyTitle")} description={t("settingsAiPrivacyBody")}>
{failed ? <Alert severity="error" sx={{ mb: 1.5 }} action={!settings ? <Button color="inherit" disabled={loading} onClick={() => { retryFocus.current = document.activeElement as HTMLElement; setFailed(false); setLoading(true); setReadAttempt(value => value + 1); }}>{t("retry")}</Button> : undefined}>{t(settings ? "settingsAiSaveFailed" : "settingsAiPrivacyUnavailable")}</Alert> : null}
{!settings ? loading ? <Box role="status" aria-label={t("loading")}><Skeleton variant="rounded" height={96} /></Box> : null : <Box ref={body} role="group" aria-label={t("settingsAiPrivacyTitle")} aria-busy={saving} tabIndex={-1} sx={{ display: "grid", gap: 1 }}>
<FormControlLabel
@@ -93,7 +93,7 @@ export default function AiUsageCard() {
const storagePercent = usage && usage.storageLimitBytes > 0 ? Math.min(100, usage.storageUsedBytes / usage.storageLimitBytes * 100) : 0;
return (
<PageSection surface title={t("settingsUsageTitle")} actions={usage ? <Typography variant="caption" sx={{ textTransform: "capitalize", fontWeight: 700 }}>{t("settingsUsagePlan", { plan: usage.plan })}</Typography> : undefined}>
<PageSection title={t("settingsUsageTitle")} actions={usage ? <Typography variant="caption" sx={{ textTransform: "capitalize", fontWeight: 700 }}>{t("settingsUsagePlan", { plan: usage.plan })}</Typography> : undefined}>
<Box ref={body} role="group" aria-label={t("settingsUsageTitle")} tabIndex={-1}>
{failed ? <Alert severity="warning" action={<Button color="inherit" onClick={() => { if (body.current?.contains(document.activeElement)) body.current.focus(); setFailed(false); setUsageAttempt(value => value + 1); }}>{t("retry")}</Button>}>{t("settingsUsageUnavailable")}</Alert> : !usage ? <Box role="status" aria-label={t("loading")}><Skeleton variant="rounded" height={150} /></Box> : null}
{usage ? <>
+1 -1
View File
@@ -130,7 +130,7 @@ export default function BackupCard() {
};
return (
<PageSection surface title={t("backupTitle")}>
<PageSection title={t("backupTitle")}>
<Box ref={body} role="group" aria-label={t("backupTitle")} tabIndex={-1} aria-busy={downloading || exportingAccount || deletingAccount}>
<Typography sx={{ color: "text.secondary", mb: 1 }}>
{t("accountExportBody")}
@@ -42,6 +42,11 @@ export default function ProFeatureNotice({ featureKey, title, children }: Props)
return (
<Alert
severity="info"
sx={{
display: "grid", gridTemplateColumns: "auto minmax(0, 1fr)",
"& .MuiAlert-message": { gridColumn: 2, gridRow: 1, minWidth: 0, overflow: "visible", overflowWrap: "anywhere" },
"& .MuiAlert-action": { gridColumn: 2, gridRow: 2, m: 0, p: 0, mt: 0.5, justifySelf: "start" },
}}
action={(
<Stack direction="row" spacing={0.5} alignItems="center">
<Button href="/settings" size="small">{aiUnavailable ? i18n?.t("aiUnavailableSettings") ?? "Review AI settings" : i18n?.t("proFeatureView") ?? "View Pro"}</Button>
@@ -36,7 +36,7 @@ export default function RulesSettingsCard({ draft, loadError, saveError, invalid
const { t } = useI18n();
if (!draft) {
return (
<PageSection surface title={t("rulesTitle")}>
<PageSection title={t("rulesTitle")}>
{loadError ? <Alert severity="error" action={<Button color="inherit" onClick={onRetry}>{t("retry")}</Button>}>{loadError}</Alert> : <Typography role="status" sx={{ color: "text.secondary" }}>{t("rulesLoading")}</Typography>}
</PageSection>
);
@@ -53,7 +53,7 @@ export default function RulesSettingsCard({ draft, loadError, saveError, invalid
});
return (
<PageSection surface title={t("rulesTitle")} description={t("rulesBody")}>
<PageSection title={t("rulesTitle")} description={t("rulesBody")}>
<Box component="form" noValidate onSubmit={(event) => { event.preventDefault(); onSave(); }}>
{saveError ? <Alert severity="error" sx={{ mb: 2 }}>{t("rulesSaveFailed")}</Alert> : null}
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" }, gap: 2 }}>
@@ -39,6 +39,7 @@ interface Props {
onColumnsChange: (next: JobTableColumns) => void;
themeMode: ThemeModePref;
onThemeModeChange: (v: ThemeModePref) => void;
onAiPreferencesSaved?: () => Promise<void>;
}
function TabPanel({ value, index, label, children, keepMounted = false }: { value: number; index: number; label: string; children: React.ReactNode; keepMounted?: boolean }) {
@@ -63,6 +64,7 @@ export default function SettingsView({
onColumnsChange,
themeMode,
onThemeModeChange,
onAiPreferencesSaved,
}: Props) {
const navigate = useNavigate();
const location = useLocation();
@@ -227,7 +229,7 @@ export default function SettingsView({
const notificationReadStatus = (notificationError?.cause as { response?: { status?: number } } | undefined)?.response?.status;
const notificationCard = (
<PageSection surface title={t("settingsNotificationsTitle")} description={t("settingsNotificationsBody")}>
<PageSection title={t("settingsNotificationsTitle")} description={t("settingsNotificationsBody")}>
<Box ref={notificationBody} role="group" aria-label={t("settingsNotificationsTitle")} tabIndex={-1} aria-busy={loadingNotifications || savingNotifications} sx={{ minWidth: 0 }}>
{!notificationPrefs ? <ViewStateNotice compact loading={loadingNotifications} title={t("settingsNotificationsLoadFailed")} error={notificationError ? {
kind: notificationReadStatus === 401 || notificationReadStatus === 403 ? "unauthorized" : "error",
@@ -287,7 +289,7 @@ export default function SettingsView({
<TabPanel value={tab} index={0} label={t("settingsTabGeneral")} keepMounted>
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "repeat(2, minmax(0, 1fr))" }, gap: "var(--app-section-gap)" }}>
<PageSection surface title={t("settingsAppearance")}>
<PageSection title={t("settingsAppearance")}>
<FormControl fullWidth>
<InputLabel id="theme-mode-label">{t("settingsTheme")}</InputLabel>
<Select
@@ -303,7 +305,7 @@ export default function SettingsView({
</FormControl>
</PageSection>
<PageSection surface title={t("settingsLanguageTitle")} description={t("settingsLanguageBody")}>
<PageSection title={t("settingsLanguageTitle")} description={t("settingsLanguageBody")}>
<FormControl fullWidth>
<InputLabel id="language-label">{t("settingsPreferredLanguage")}</InputLabel>
<Select
@@ -319,7 +321,7 @@ export default function SettingsView({
</PageSection>
</Box>
<PageSection surface title={t("settingsJobs")}>
<PageSection title={t("settingsJobs")}>
<Box sx={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
<Box sx={{ flex: "1 1 15rem", minWidth: 0 }}>
<Typography component="h3" variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
@@ -366,10 +368,10 @@ export default function SettingsView({
</Box>
</PageSection>
<Box ref={aiControls} id="settings-ai-controls" role="group" aria-label={t("settingsAiPrivacyTitle")} tabIndex={-1}><AiPrivacySettingsCard /></Box>
<Box ref={aiControls} id="settings-ai-controls" role="group" aria-label={t("settingsAiPrivacyTitle")} tabIndex={-1}><AiPrivacySettingsCard onSaved={onAiPreferencesSaved} /></Box>
<AiUsageCard />
<QuickCaptureCard />
<PageSection surface title={t("connectedAccounts")} description={t("settingsConnectedAccountsBody")}
<PageSection title={t("connectedAccounts")} description={t("settingsConnectedAccountsBody")}
actions={<Button variant="outlined" onClick={() => navigate("/settings/connected-accounts")}>{t("settingsManageConnectedAccounts")}</Button>} />
</Box>
</TabPanel>
@@ -389,7 +391,7 @@ export default function SettingsView({
<TabPanel value={tab} index={4} label={t("settingsTabAutomation")}>
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
<PageSection surface title={t("settingsTabAutomation")} description={t("settingsAutomationBody")}>
<PageSection title={t("settingsTabAutomation")} description={t("settingsAutomationBody")}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>{t("settingsAutomationEmailSchedule")}</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
<Button onClick={() => { selectSection(0); requestAnimationFrame(() => aiControls.current?.focus()); }}>{t("settingsAutomationAi")}</Button>