diff --git a/web/packages/base/components/mui/ActivityIndicator.tsx b/web/packages/base/components/mui/ActivityIndicator.tsx index e7633d71f9..b52980012d 100644 --- a/web/packages/base/components/mui/ActivityIndicator.tsx +++ b/web/packages/base/components/mui/ActivityIndicator.tsx @@ -1,9 +1,11 @@ +import ErrorOutline from "@mui/icons-material/ErrorOutline"; import { CircularProgress, Stack, Typography, type CircularProgressProps, } from "@mui/material"; +import { t } from "i18next"; import type React from "react"; /** @@ -27,3 +29,21 @@ export const ActivityIndicator: React.FC< ) : ( ); + +/** + * An error message indicator, styled to complement {@link ActivityIndicator}. + * + * If a child is provided, it is used as the error message to show (after being + * wrapped in a suitable {@link Typography}). Otherwise the default generic + * error message is shown. + */ +export const ErrorIndicator: React.FC = ({ + children, +}) => ( + + + + {children ?? t("generic_error")} + + +); diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 09ca85c1da..6c53e054e0 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -1,4 +1,7 @@ -import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { + ActivityIndicator, + ErrorIndicator, +} from "@/base/components/mui/ActivityIndicator"; import { CenteredBox } from "@/base/components/mui/Container"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { LoadingButton } from "@/base/components/mui/LoadingButton"; @@ -250,10 +253,10 @@ const SuggestionsDialog: React.FC = ({ onClose, person, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog, onGenericError } = useAppContext(); const [phase, setPhase] = useState< - "loading" | "failed" | "saving" | undefined + "loading" | "fetch-failed" | "saving" | undefined >(); const [suggestions, setSuggestions] = useState([]); const [unsavedChanges /*, setUnsavedChanges */] = useState(false); @@ -292,7 +295,7 @@ const SuggestionsDialog: React.FC = ({ resetPhaseAndClose(); } catch (e) { log.error("Failed to save suggestion review", e); - setPhase("failed"); + onGenericError(e); } }; @@ -302,14 +305,28 @@ const SuggestionsDialog: React.FC = ({ setPhase("loading"); let ignore = false; - void suggestionsForPerson(person).then(async (suggestions) => { - setPhase(undefined); - if (!ignore) setSuggestions(suggestions); - }); + + const go = async () => { + try { + const suggestions = await suggestionsForPerson(person); + if (!ignore) { + setPhase(undefined); + setSuggestions(suggestions); + } + } catch (e) { + log.error("Failed to generate suggestions", e); + if (!ignore) { + setPhase("fetch-failed"); + } + } + }; + + void go(); + return () => { ignore = true; }; - }, [open, person]); + }, [open, person, onGenericError]); console.log({ open, person }); @@ -332,6 +349,10 @@ const SuggestionsDialog: React.FC = ({ {pt("Finding similar faces...")} + ) : phase == "fetch-failed" ? ( + + + ) : suggestions.length == 0 ? (