feat(ui): add automation settings and safe AI review presentation
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
Reference in New Issue
Block a user