feat(ui): add automation settings and safe AI review presentation

This commit is contained in:
cesnimda
2026-11-03 19:28:02 +01:00
parent a88310481f
commit e2327c1fa2
121 changed files with 4445 additions and 47 deletions
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import CloseIcon from "@mui/icons-material/Close";
import { Alert, Button, IconButton, Stack, Typography } from "@mui/material";
import { useOptionalI18n } from "../i18n/I18nProvider";
import { useAccountPlan } from "../accountPlan";
type Props = {
featureKey: string;
@@ -15,6 +16,9 @@ function storageKey(featureKey: string) {
export default function ProFeatureNotice({ featureKey, title, children }: Props) {
const i18n = useOptionalI18n();
const { plan, canUseAi } = useAccountPlan();
const aiUnavailable = plan === "pro" && !canUseAi;
const displayTitle = aiUnavailable ? i18n?.t("aiUnavailableTitle") ?? "AI assistance is unavailable" : title;
const [dismissed, setDismissed] = useState(() => {
if (typeof window === "undefined") return false;
try {
@@ -40,15 +44,15 @@ export default function ProFeatureNotice({ featureKey, title, children }: Props)
severity="info"
action={(
<Stack direction="row" spacing={0.5} alignItems="center">
<Button href="/settings" size="small">{i18n?.t("proFeatureView") ?? "View Pro"}</Button>
<IconButton size="small" aria-label={i18n?.t("proFeatureDismiss", { title }) ?? `Dismiss ${title}`} onClick={dismiss}>
<Button href="/settings" size="small">{aiUnavailable ? i18n?.t("aiUnavailableSettings") ?? "Review AI settings" : i18n?.t("proFeatureView") ?? "View Pro"}</Button>
<IconButton size="small" aria-label={i18n?.t("proFeatureDismiss", { title: displayTitle }) ?? `Dismiss ${displayTitle}`} onClick={dismiss}>
<CloseIcon fontSize="small" />
</IconButton>
</Stack>
)}
>
<Typography component="span" sx={{ fontWeight: 800 }}>{title}</Typography>{" "}
{children}
<Typography component="span" sx={{ fontWeight: 800 }}>{displayTitle}</Typography>{" "}
{aiUnavailable ? i18n?.t("aiUnavailableBody") ?? "You can continue editing manually. Review your AI settings and usage before trying again." : children}
</Alert>
);
}
+53 -30
View File
@@ -10,6 +10,7 @@ import {
InputLabel,
MenuItem,
Select,
Switch,
Tab,
Tabs,
Typography,
@@ -81,6 +82,7 @@ export default function SettingsView({
const rulesSession = useRef({ mounted: true, loading: false, saving: false });
const notificationSession = useRef({ mounted: true, loading: false, saving: false, generation: 0 });
const notificationBody = useRef<HTMLDivElement>(null);
const aiControls = useRef<HTMLDivElement>(null);
const notificationRetryFocus = useRef<HTMLElement | null>(null);
const currentTranslation = useRef(t);
const rulesDirty = Boolean(rulesDraft && savedRules && RULE_FIELDS.some(field => rulesDraft[field] !== String(savedRules[field])));
@@ -223,6 +225,36 @@ 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")}>
<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",
message: getApiErrorMessage(notificationError.cause, t("settingsNotificationsLoadFailed")),
retryable: notificationReadStatus !== 401 && notificationReadStatus !== 403,
} : null} onRetry={retryNotifications} /> : <Box sx={{ display: "grid", gap: 1 }}>
{notificationError?.action ? <Alert severity="error" sx={{ mb: 1.5, overflowWrap: "anywhere" }}>{getApiErrorMessage(notificationError.cause, t("settingsNotificationsSaveFailed"))}</Alert> : null}
{notificationsDirty ? <Typography role="status" variant="body2" color="text.secondary">{t("assetsUnsavedChanges")}. {t("settingsNotificationsDraftScope")}</Typography> : null}
<FormControlLabel
control={<Switch checked={notificationPrefs.emailFollowUpRemindersEnabled} onChange={(e) => setNotificationPrefs({ emailFollowUpRemindersEnabled: e.target.checked })} />}
label={t("settingsNotificationsFollowUpReminders")}
/>
<Typography variant="caption" color="text.secondary">{t("settingsNotificationsEmailBody")}</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
<Button disabled={!notificationsDirty || savingNotifications} onClick={discardNotifications}>{t("assetsDiscardChanges")}</Button>
<Button variant="contained" disabled={savingNotifications} onClick={() => void saveNotifications()}>{savingNotifications ? t("settingsSaving") : t("settingsNotificationsSave")}</Button>
</Box>
</Box>}
</Box>
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 1.5 }}>
{t("settingsNotificationsDelivery")}
</Typography>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
</Box>
</PageSection>
);
return (
<Box sx={{ minWidth: 0 }}>
<FormControl fullWidth sx={{ display: { xs: "flex", sm: "none" }, mb: 1 }}>
@@ -234,6 +266,7 @@ export default function SettingsView({
onChange={(event) => selectSection(Number(event.target.value))}
>
<MenuItem value={0}>{t("settingsTabGeneral")}</MenuItem>
<MenuItem value={4}>{t("settingsTabAutomation")}</MenuItem>
<MenuItem value={1}>{t("settingsTabFollowUps")}</MenuItem>
<MenuItem value={2}>{t("settingsTabNotifications")}</MenuItem>
<MenuItem value={3}>{t("settingsTabBackup")}</MenuItem>
@@ -241,10 +274,11 @@ export default function SettingsView({
</FormControl>
<Tabs aria-label={t("settings")} value={tab} onChange={(_, v) => selectSection(v)} variant="scrollable" scrollButtons="auto" sx={{ display: { xs: "none", sm: "flex" }, mb: 1 }}>
<Tab id="settings-tab-0" aria-controls="settings-panel-0" label={t("settingsTabGeneral")} />
<Tab id="settings-tab-1" aria-controls="settings-panel-1" label={t("settingsTabFollowUps")} />
<Tab id="settings-tab-2" aria-controls="settings-panel-2" label={t("settingsTabNotifications")} />
<Tab id="settings-tab-3" aria-controls="settings-panel-3" label={t("settingsTabBackup")} />
<Tab value={0} id="settings-tab-0" aria-controls="settings-panel-0" label={t("settingsTabGeneral")} />
<Tab value={4} id="settings-tab-4" aria-controls="settings-panel-4" label={t("settingsTabAutomation")} />
<Tab value={1} id="settings-tab-1" aria-controls="settings-panel-1" label={t("settingsTabFollowUps")} />
<Tab value={2} id="settings-tab-2" aria-controls="settings-panel-2" label={t("settingsTabNotifications")} />
<Tab value={3} id="settings-tab-3" aria-controls="settings-panel-3" label={t("settingsTabBackup")} />
</Tabs>
{rulesDirty ? <Typography role="status" variant="body2" sx={{ mt: 1, color: "text.secondary" }}>{t("assetsUnsavedChanges")}. {t("rulesDraftScope")}</Typography> : null}
@@ -331,7 +365,7 @@ export default function SettingsView({
</Box>
</PageSection>
<AiPrivacySettingsCard />
<Box ref={aiControls} id="settings-ai-controls" role="group" aria-label={t("settingsAiPrivacyTitle")} tabIndex={-1}><AiPrivacySettingsCard /></Box>
<AiUsageCard />
<QuickCaptureCard />
<PageSection surface title={t("connectedAccounts")} description={t("settingsConnectedAccountsBody")}
@@ -349,33 +383,22 @@ export default function SettingsView({
</TabPanel>
<TabPanel value={tab} index={2} label={t("settingsTabNotifications")}>
<PageSection surface 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",
message: getApiErrorMessage(notificationError.cause, t("settingsNotificationsLoadFailed")),
retryable: notificationReadStatus !== 401 && notificationReadStatus !== 403,
} : null} onRetry={retryNotifications} /> : <Box sx={{ display: "grid", gap: 1 }}>
{notificationError?.action ? <Alert severity="error" sx={{ mb: 1.5, overflowWrap: "anywhere" }}>{getApiErrorMessage(notificationError.cause, t("settingsNotificationsSaveFailed"))}</Alert> : null}
{notificationsDirty ? <Typography role="status" variant="body2" color="text.secondary">{t("assetsUnsavedChanges")}. {t("settingsNotificationsDraftScope")}</Typography> : null}
<FormControlLabel
control={<Checkbox checked={notificationPrefs.emailFollowUpRemindersEnabled} onChange={(e) => setNotificationPrefs({ emailFollowUpRemindersEnabled: e.target.checked })} />}
label={t("settingsNotificationsFollowUpReminders")}
/>
<Typography variant="caption" color="text.secondary">{t("settingsNotificationsEmailBody")}</Typography>
{notificationCard}
</TabPanel>
<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")}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>{t("settingsAutomationEmailSchedule")}</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
<Button disabled={!notificationsDirty || savingNotifications} onClick={discardNotifications}>{t("assetsDiscardChanges")}</Button>
<Button variant="contained" disabled={savingNotifications} onClick={() => void saveNotifications()}>{savingNotifications ? t("settingsSaving") : t("settingsNotificationsSave")}</Button>
<Button onClick={() => { selectSection(0); requestAnimationFrame(() => aiControls.current?.focus()); }}>{t("settingsAutomationAi")}</Button>
<Button onClick={() => selectSection(1)}>{t("settingsTabFollowUps")}</Button>
<Button onClick={() => navigate("/discover")}>{t("settingsAutomationSearches")}</Button>
</Box>
</Box>}
</Box>
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 1.5 }}>
{t("settingsNotificationsDelivery")}
</Typography>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
</Box>
</PageSection>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}>{t("settingsAutomationRulesScope")}</Typography>
</PageSection>
{notificationCard}
</Box>
</TabPanel>
<TabPanel value={tab} index={3} label={t("settingsTabBackup")}>