docs: add setup guides and error notification component
- Add OpenFang Kernel configuration guide (docs/setup/OPENFANG-SETUP.md) - Add Chinese models configuration guide (docs/setup/chinese-models.md) - Add quick start guide (docs/quick-start.md) - Add quick start scripts for Windows and Linux/macOS - Add ErrorNotification component for centralized error display These additions help users: - Quickly set up development environment - Configure OpenFang backend correctly - Configure Chinese LLM providers (GLM, Qwen, Kimi, MiniMax) - See error notifications in a consistent UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
271
desktop/src/components/ErrorNotification.tsx
Normal file
271
desktop/src/components/ErrorNotification.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* ErrorNotification Component
|
||||
*
|
||||
* Displays error notifications as toast-style messages.
|
||||
* Integrates with the centralized error handling system.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
X,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Bug,
|
||||
WifiOff,
|
||||
ShieldAlert,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getUndismissedErrors,
|
||||
dismissError,
|
||||
dismissAll,
|
||||
type StoredError,
|
||||
} from '../lib/error-handling';
|
||||
import {
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
formatErrorForToast,
|
||||
} from '../lib/error-types';
|
||||
|
||||
interface ErrorNotificationProps {
|
||||
/** Maximum number of visible notifications */
|
||||
maxVisible?: number;
|
||||
/** Position on screen */
|
||||
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
||||
/** Auto dismiss timeout in ms (0 = no auto dismiss) */
|
||||
autoDismissMs?: number;
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const categoryIcons: Record<ErrorCategory, typeof AlertCircle> = {
|
||||
network: WifiOff,
|
||||
authentication: ShieldAlert,
|
||||
authorization: ShieldAlert,
|
||||
validation: AlertTriangle,
|
||||
configuration: AlertTriangle,
|
||||
internal: Bug,
|
||||
external: AlertCircle,
|
||||
timeout: Clock,
|
||||
unknown: AlertCircle,
|
||||
};
|
||||
|
||||
const severityColors: Record<ErrorSeverity, {
|
||||
bg: string;
|
||||
border: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
}> = {
|
||||
critical: {
|
||||
bg: 'bg-red-50 dark:bg-red-900/20',
|
||||
border: 'border-red-200 dark:border-red-800',
|
||||
text: 'text-red-800 dark:text-red-200',
|
||||
icon: 'text-red-500',
|
||||
},
|
||||
high: {
|
||||
bg: 'bg-orange-50 dark:bg-orange-900/20',
|
||||
border: 'border-orange-200 dark:border-orange-800',
|
||||
text: 'text-orange-800 dark:text-orange-200',
|
||||
icon: 'text-orange-500',
|
||||
},
|
||||
medium: {
|
||||
bg: 'bg-yellow-50 dark:bg-yellow-900/20',
|
||||
border: 'border-yellow-200 dark:border-yellow-800',
|
||||
text: 'text-yellow-800 dark:text-yellow-200',
|
||||
icon: 'text-yellow-500',
|
||||
},
|
||||
low: {
|
||||
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
||||
border: 'border-blue-200 dark:border-blue-800',
|
||||
text: 'text-blue-800 dark:text-blue-200',
|
||||
icon: 'text-blue-500',
|
||||
},
|
||||
};
|
||||
|
||||
function ErrorItem({
|
||||
error,
|
||||
onDismiss,
|
||||
autoDismissMs,
|
||||
}: {
|
||||
error: StoredError;
|
||||
onDismiss: (id: string) => void;
|
||||
autoDismissMs: number;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const Icon = categoryIcons[error.category] || AlertCircle;
|
||||
const colors = severityColors[error.severity] || severityColors.medium;
|
||||
const { title, message } = formatErrorForToast(error);
|
||||
|
||||
// Auto dismiss
|
||||
useEffect(() => {
|
||||
if (autoDismissMs > 0 && error.severity !== 'critical') {
|
||||
const timer = setTimeout(() => {
|
||||
onDismiss(error.id);
|
||||
}, autoDismissMs);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [autoDismissMs, error.id, error.severity, onDismiss]);
|
||||
|
||||
const hasDetails = error.stack || error.context;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 300, scale: 0.9 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 300, scale: 0.9 }}
|
||||
className={`
|
||||
${colors.bg} ${colors.border} ${colors.text}
|
||||
border rounded-lg shadow-lg p-4 min-w-[320px] max-w-[420px]
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Icon className={`w-5 h-5 mt-0.5 flex-shrink-0 ${colors.icon}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h4 className="font-medium text-sm">{title}</h4>
|
||||
<button
|
||||
onClick={() => onDismiss(error.id)}
|
||||
className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 flex-shrink-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm mt-1 opacity-90">{message}</p>
|
||||
|
||||
{hasDetails && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-1 text-xs mt-2 opacity-70 hover:opacity-100"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
隐藏详情
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
显示详情
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{expanded && hasDetails && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="mt-2 p-2 bg-black/5 dark:bg-white/5 rounded text-xs font-mono overflow-auto max-h-32"
|
||||
>
|
||||
{error.context && (
|
||||
<div className="mb-1">
|
||||
<span className="opacity-70">Context: </span>
|
||||
{JSON.stringify(error.context, null, 2)}
|
||||
</div>
|
||||
)}
|
||||
{error.stack && (
|
||||
<pre className="whitespace-pre-wrap opacity-70">{error.stack}</pre>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 text-xs opacity-60">
|
||||
<span>{error.category}</span>
|
||||
<span>•</span>
|
||||
<span>{new Date(error.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorNotification({
|
||||
maxVisible = 3,
|
||||
position = 'top-right',
|
||||
autoDismissMs = 10000,
|
||||
className = '',
|
||||
}: ErrorNotificationProps) {
|
||||
const [errors, setErrors] = useState<StoredError[]>([]);
|
||||
|
||||
// Poll for new errors
|
||||
useEffect(() => {
|
||||
const updateErrors = () => {
|
||||
setErrors(getUndismissedErrors().slice(0, maxVisible));
|
||||
};
|
||||
|
||||
updateErrors();
|
||||
const interval = setInterval(updateErrors, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [maxVisible]);
|
||||
|
||||
const handleDismiss = useCallback((id: string) => {
|
||||
dismissError(id);
|
||||
setErrors(prev => prev.filter(e => e.id !== id));
|
||||
}, []);
|
||||
|
||||
const handleDismissAll = useCallback(() => {
|
||||
dismissAll();
|
||||
setErrors([]);
|
||||
}, []);
|
||||
|
||||
const positionClasses: Record<string, string> = {
|
||||
'top-right': 'top-4 right-4',
|
||||
'top-left': 'top-4 left-4',
|
||||
'bottom-right': 'bottom-4 right-4',
|
||||
'bottom-left': 'bottom-4 left-4',
|
||||
};
|
||||
|
||||
if (errors.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed ${positionClasses[position]} z-50 flex flex-col gap-2 ${className}`}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{errors.map(error => (
|
||||
<ErrorItem
|
||||
key={error.id}
|
||||
error={error}
|
||||
onDismiss={handleDismiss}
|
||||
autoDismissMs={autoDismissMs}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{errors.length > 1 && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
onClick={handleDismissAll}
|
||||
className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-center py-1"
|
||||
>
|
||||
清除全部 ({errors.length})
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorNotificationProvider - Include at app root
|
||||
*/
|
||||
export function ErrorNotificationProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<ErrorNotification />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorNotification;
|
||||
Reference in New Issue
Block a user