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:
iven
2026-03-21 00:17:44 +08:00
parent c5d91cf9f0
commit d3a4de2480
6 changed files with 1931 additions and 0 deletions

View 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;