Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| import { | |
| createContext, | |
| useContext, | |
| useState, | |
| useCallback, | |
| type ReactNode, | |
| } from "react"; | |
| import ErrorModal, { type ErrorModalHeadingVariant } from "../components/ErrorModal"; | |
| export const DEFAULT_ERROR_MESSAGE = | |
| "We got an unexpected error, please try again or contact support."; | |
| /** | |
| * Shown when the backend is briefly unreachable — most commonly while a new | |
| * version is being deployed (old container is torn down before the new one is | |
| * ready) or behind a gateway returning 502/503/504. Friendlier than the raw | |
| * axios "Network Error" string. | |
| */ | |
| export const MAINTENANCE_MESSAGE = | |
| "Our system is being updated at the moment. Please try again in a few minutes. We apologise for the inconvenience."; | |
| /** | |
| * True when the error means "we couldn't reach the server" rather than "the | |
| * server rejected the request". Covers network errors (no response at all) and | |
| * gateway statuses that occur during a deploy/rollout. | |
| */ | |
| export function isMaintenanceError(err: unknown): boolean { | |
| if (!err || typeof err !== "object") return false; | |
| const e = err as { | |
| code?: string; | |
| message?: string; | |
| response?: { status?: number }; | |
| }; | |
| const status = e.response?.status; | |
| if (status === 502 || status === 503 || status === 504) return true; | |
| // No response means the request never reached a server (connection refused / | |
| // DNS / container being swapped). Axios reports this as ERR_NETWORK. | |
| if (!e.response) { | |
| if (e.code === "ERR_NETWORK") return true; | |
| if (typeof e.message === "string" && /network error/i.test(e.message)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| export type ErrorModalVariant = ErrorModalHeadingVariant; | |
| interface ErrorOptions { | |
| showUpgrade?: boolean; | |
| /** Generation pipeline failures (scrape/script/scene) use a softer “Oops” heading. */ | |
| variant?: ErrorModalHeadingVariant; | |
| /** Runs after the user dismisses the modal (OK, backdrop click, or upgrade redirect). */ | |
| onClose?: () => void; | |
| } | |
| interface ErrorModalContextType { | |
| showError: (message: string, options?: ErrorOptions) => void; | |
| } | |
| const ErrorModalContext = createContext<ErrorModalContextType | null>(null); | |
| export function ErrorModalProvider({ children }: { children: ReactNode }) { | |
| const [message, setMessage] = useState<string | null>(null); | |
| const [showUpgrade, setShowUpgrade] = useState(false); | |
| const [variant, setVariant] = useState<ErrorModalHeadingVariant>("default"); | |
| const [onCloseAction, setOnCloseAction] = useState<(() => void) | null>(null); | |
| const showError = useCallback((msg: string, options?: ErrorOptions) => { | |
| const finalMsg = msg && msg.trim() ? msg : DEFAULT_ERROR_MESSAGE; | |
| setMessage(finalMsg); | |
| setShowUpgrade(Boolean(options?.showUpgrade)); | |
| let nextVariant: ErrorModalHeadingVariant = | |
| options?.variant === "pipeline" || options?.variant === "warning" | |
| ? options.variant | |
| : "default"; | |
| if (options?.variant === "maintenance" || finalMsg === MAINTENANCE_MESSAGE) { | |
| nextVariant = "maintenance"; | |
| } | |
| setVariant(nextVariant); | |
| setOnCloseAction(() => options?.onClose ?? null); | |
| }, []); | |
| const close = useCallback(() => { | |
| setMessage(null); | |
| setShowUpgrade(false); | |
| setVariant("default"); | |
| const fn = onCloseAction; | |
| setOnCloseAction(null); | |
| try { | |
| fn?.(); | |
| } catch { | |
| // Modal close should never crash the UI. | |
| } | |
| }, [onCloseAction]); | |
| return ( | |
| <ErrorModalContext.Provider value={{ showError }}> | |
| {children} | |
| <ErrorModal | |
| open={message != null} | |
| message={message ?? ""} | |
| variant={variant} | |
| showUpgrade={showUpgrade} | |
| onClose={close} | |
| /> | |
| </ErrorModalContext.Provider> | |
| ); | |
| } | |
| export function useErrorModal(): ErrorModalContextType { | |
| const ctx = useContext(ErrorModalContext); | |
| if (!ctx) { | |
| throw new Error("useErrorModal must be used within ErrorModalProvider"); | |
| } | |
| return ctx; | |
| } | |
| /** Extract a user-facing error message from an API/axios error or a thrown Error. */ | |
| export function getErrorMessage( | |
| err: unknown, | |
| fallback = DEFAULT_ERROR_MESSAGE | |
| ): string { | |
| // Server unreachable (e.g. mid-deploy container swap) → friendly notice | |
| // instead of the raw "Network Error" string. | |
| if (isMaintenanceError(err)) return MAINTENANCE_MESSAGE; | |
| if (err && typeof err === "object" && "response" in err) { | |
| const res = (err as { response?: { data?: { detail?: string | { message?: string } } } }).response; | |
| const detail = res?.data?.detail; | |
| if (typeof detail === "string" && detail.trim()) return detail; | |
| if ( | |
| detail && | |
| typeof detail === "object" && | |
| "message" in detail && | |
| typeof detail.message === "string" && | |
| detail.message.trim() | |
| ) { | |
| return detail.message; | |
| } | |
| } | |
| if (err instanceof Error && err.message.trim()) { | |
| return err.message; | |
| } | |
| return fallback; | |
| } | |