MicProtocol / apps /web /src /components /ui /ErrorState.tsx
Benaah's picture
Upload folder using huggingface_hub
fb12607 verified
Raw History Blame Contribute Delete
9.18 kB
"use client";
import React, { useState } from "react";
import {
AlertTriangle,
RotateCcw,
ChevronDown,
ChevronUp,
HelpCircle,
WifiOff,
ShieldAlert,
Clock,
} from "lucide-react";
import { parseError, FormattedError } from "@/lib/error-utils";
export interface ErrorStateProps {
title?: string;
description?: string;
error?: Error | unknown;
/** Action called when clicking "Try Again" */
onRetry?: () => void;
/** Custom action slot */
action?: React.ReactNode;
/** Compact presentation for cards/widgets */
compact?: boolean;
/** Whether to show technical diagnostic details */
showDiagnostics?: boolean;
className?: string;
style?: React.CSSProperties;
}
/**
* EasyUI-inspired ErrorState component for widgets, cards, and page sections.
* Features Neo-Industrial dark aesthetics, spring-like pulse icon, and collapsible diagnostic trace.
*/
export function ErrorState({
title: customTitle,
description: customDescription,
error,
onRetry,
action,
compact = false,
showDiagnostics = true,
className = "",
style,
}: ErrorStateProps) {
const [showDetails, setShowDetails] = useState(false);
const formatted: FormattedError = parseError(error);
const displayTitle = customTitle || formatted.title;
const displayDescription = customDescription || formatted.message;
// Select contextual icon based on error category
let IconComponent = AlertTriangle;
let accentColor = "var(--color-error, #EF4444)";
if (formatted.isNetworkError) {
IconComponent = WifiOff;
accentColor = "var(--color-warning, #F59E0B)";
} else if (formatted.isAuthError || formatted.isForbidden) {
IconComponent = ShieldAlert;
accentColor = "var(--color-secondary, #8B5CF6)";
} else if (formatted.isRateLimit) {
IconComponent = Clock;
accentColor = "var(--color-warning, #F59E0B)";
}
if (compact) {
return (
<div
role="alert"
className={`error-state-compact ${className}`}
style={{
display: "flex",
alignItems: "center",
gap: "var(--space-2, 0.5rem)",
padding: "var(--space-2, 0.5rem) var(--space-3, 0.75rem)",
background: "rgba(239, 68, 68, 0.06)",
border: "1px solid rgba(239, 68, 68, 0.2)",
borderRadius: "var(--radius-sm, 2px)",
fontSize: "var(--text-xs, 0.75rem)",
color: "var(--text-secondary, #A7A3B4)",
...style,
}}
>
<IconComponent size={14} color={accentColor} style={{ flexShrink: 0 }} />
<span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{displayDescription}
</span>
{onRetry && (
<button
type="button"
onClick={onRetry}
style={{
background: "transparent",
border: "none",
color: accentColor,
fontWeight: 600,
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: 3,
padding: 0,
}}
>
<RotateCcw size={11} /> Retry
</button>
)}
</div>
);
}
return (
<div
role="alert"
className={`error-state-container ${className}`}
style={{
position: "relative",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
padding: "var(--space-8, 2rem) var(--space-6, 1.5rem)",
background: "var(--bg-elevated, #17151F)",
border: "1px solid var(--border-color, #272430)",
borderRadius: "var(--radius-md, 4px)",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.35)",
overflow: "hidden",
...style,
}}
>
{/* Top accent glow line */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 2,
background: `linear-gradient(90deg, transparent, ${accentColor}, transparent)`,
opacity: 0.8,
}}
/>
{/* Pulsing Icon Badge */}
<div
style={{
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 52,
height: 52,
borderRadius: "var(--radius-md, 4px)",
background: `color-mix(in srgb, ${accentColor} 12%, transparent)`,
border: `1px solid color-mix(in srgb, ${accentColor} 30%, transparent)`,
color: accentColor,
marginBottom: "var(--space-4, 1rem)",
}}
>
<IconComponent size={26} strokeWidth={2.2} />
</div>
{/* Title */}
<h3
style={{
fontFamily: "var(--font-display, inherit)",
fontSize: "var(--text-base, 1rem)",
fontWeight: 700,
color: "var(--text-primary, #F6F4F8)",
marginBottom: "var(--space-1, 0.25rem)",
letterSpacing: "-0.01em",
}}
>
{displayTitle}
</h3>
{/* Description */}
<p
style={{
fontSize: "var(--text-sm, 0.875rem)",
color: "var(--text-secondary, #A7A3B4)",
maxWidth: 460,
lineHeight: 1.55,
marginBottom: onRetry || action ? "var(--space-5, 1.25rem)" : 0,
}}
>
{displayDescription}
</p>
{/* Status & Code Pill if available */}
{formatted.code && (
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "2px 8px",
background: "rgba(255, 255, 255, 0.04)",
border: "1px solid var(--border-color-subtle, #1C1924)",
borderRadius: "var(--radius-sm, 2px)",
fontSize: "11px",
fontFamily: "var(--font-mono, monospace)",
color: "var(--text-tertiary, #6D697C)",
marginBottom: "var(--space-4, 1rem)",
}}
>
<span>CODE: {formatted.code}</span>
{formatted.statusCode && <span>({formatted.statusCode})</span>}
</div>
)}
{/* Actions */}
{(onRetry || action) && (
<div
style={{
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
gap: "var(--space-3, 0.75rem)",
}}
>
{onRetry && (
<button
type="button"
onClick={onRetry}
className="btn btn-primary"
style={{
display: "inline-flex",
alignItems: "center",
gap: "var(--space-2, 0.5rem)",
fontSize: "var(--text-sm, 0.875rem)",
fontWeight: 600,
padding: "var(--space-2, 0.5rem) var(--space-4, 1rem)",
}}
>
<RotateCcw size={14} /> Try Again
</button>
)}
{action}
</div>
)}
{/* Diagnostics / Error details accordion */}
{showDiagnostics && (error || formatted.requestId) && (
<div style={{ marginTop: "var(--space-4, 1rem)", width: "100%", maxWidth: 500 }}>
<button
type="button"
onClick={() => setShowDetails(!showDetails)}
style={{
background: "transparent",
border: "none",
color: "var(--text-tertiary, #6D697C)",
fontSize: "var(--text-xs, 0.75rem)",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 4,
padding: 4,
}}
>
{showDetails ? (
<>
Hide diagnostics <ChevronUp size={12} />
</>
) : (
<>
Show diagnostics <ChevronDown size={12} />
</>
)}
</button>
{showDetails && (
<div
style={{
marginTop: "var(--space-2, 0.5rem)",
padding: "var(--space-3, 0.75rem)",
background: "rgba(0, 0, 0, 0.45)",
border: "1px solid var(--border-color-subtle, #1C1924)",
borderRadius: "var(--radius-sm, 2px)",
fontFamily: "var(--font-mono, monospace)",
fontSize: "11px",
textAlign: "left",
color: "var(--text-secondary, #A7A3B4)",
maxHeight: 180,
overflowY: "auto",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
>
{formatted.requestId && (
<div style={{ marginBottom: 4, color: "var(--color-primary, #FF9F0A)" }}>
Request ID: {formatted.requestId}
</div>
)}
{error instanceof Error ? error.stack || error.message : JSON.stringify(error, null, 2)}
</div>
)}
</div>
)}
</div>
);
}