'use client' /** * RecentIssuesList - #42 近期錯誤列表 * ==================================== * Phase 10: Sentry + OpenClaw + UI 整合 * * Nothing.tech 視覺規範: * - 純白底色 (bg-white) * - 極細淺灰邊框 (border border-gray-200) * - 無圓角或微圓角 (rounded-sm) * - 嚴禁陰影 (shadow-none) * * 建立: 2026-03-26 (台北時區) * 建立者: Claude Code (#42 Error UI) */ import { useState } from 'react' import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { AlertTriangle, Bug, Clock, Users, ChevronRight, Sparkles, ExternalLink } from 'lucide-react' import type { SentryIssue, ErrorAnalysisResponse } from '@/lib/api-client' import { apiClient } from '@/lib/api-client' // ============================================================================= // Level Config // ============================================================================= const LEVEL_CONFIG = { fatal: { color: 'text-red-700', bgColor: 'bg-red-50', borderColor: 'border-red-200', icon: AlertTriangle, }, error: { color: 'text-orange-700', bgColor: 'bg-orange-50', borderColor: 'border-orange-200', icon: Bug, }, warning: { color: 'text-amber-700', bgColor: 'bg-amber-50', borderColor: 'border-amber-200', icon: AlertTriangle, }, info: { color: 'text-blue-700', bgColor: 'bg-blue-50', borderColor: 'border-blue-200', icon: Bug, }, } as const // ============================================================================= // Component Props // ============================================================================= interface RecentIssuesListProps { issues: SentryIssue[] loading?: boolean error?: string | null onIssueClick?: (issue: SentryIssue) => void className?: string } // ============================================================================= // Issue Item Component // ============================================================================= function IssueItem({ issue, onClick, }: { issue: SentryIssue onClick?: () => void }) { const t = useTranslations('errors') const [analyzing, setAnalyzing] = useState(false) const [analysis, setAnalysis] = useState(null) const levelConfig = LEVEL_CONFIG[issue.level] || LEVEL_CONFIG.error const LevelIcon = levelConfig.icon // Format relative time (i18n compliant) const lastSeen = new Date(issue.last_seen) const now = new Date() const diffMs = now.getTime() - lastSeen.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) let timeAgo: string if (diffMins < 60) { timeAgo = t('timeAgo.minutes', { count: diffMins }) } else if (diffHours < 24) { timeAgo = t('timeAgo.hours', { count: diffHours }) } else { timeAgo = t('timeAgo.days', { count: diffDays }) } const handleAnalyze = async (e: React.MouseEvent) => { e.stopPropagation() setAnalyzing(true) try { const result = await apiClient.analyzeError(issue.id) setAnalysis(result) } catch (err) { console.error('Error analyzing issue:', err) } finally { setAnalyzing(false) } } return (
{/* Main Row */}
{/* Level Badge */}
{issue.level}
{/* Content */}
{/* Title */}
{issue.title}
{/* Culprit */} {issue.culprit && (
{issue.culprit}
)} {/* Meta */}
{issue.count.toLocaleString()} {issue.user_count} {timeAgo} {issue.short_id}
{/* Actions */}
{/* AI Analyze Button */} {/* External Link */} {issue.permalink && ( e.stopPropagation()} className="text-gray-400 hover:text-gray-600" > )}
{/* AI Analysis Result */} {analysis && analysis.status === 'completed' && analysis.analysis && (
{t('aiAnalysis')} {analysis.analysis.severity}
{t('rootCause')}: {analysis.analysis.root_cause}
{t('fixSummary')}: {analysis.analysis.fix_recommendation.summary}
{t('category')}: {analysis.analysis.category} | {t('confidence')}: {(analysis.analysis.confidence * 100).toFixed(0)}%
)}
) } // ============================================================================= // Main Component // ============================================================================= export function RecentIssuesList({ issues, loading = false, error = null, onIssueClick, className, }: RecentIssuesListProps) { const t = useTranslations('errors') // Loading state if (loading) { return (
{[1, 2, 3].map((i) => (
))}
) } // Error state if (error) { return (
{error}
) } // Empty state if (issues.length === 0) { return (

{t('noIssues')}

) } return (
{/* Header */}

{t('recentIssues')}

({issues.length})
{/* Issues List */}
{issues.map((issue) => ( onIssueClick?.(issue)} /> ))}
) }