feat(ui): simplify A3 workspace and refresh saved AI availability
This commit is contained in:
@@ -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 ? <>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user