fix(web): standardize UI icon language
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CheckCircle2, Clock, XCircle, Loader2, Inbox } from 'lucide-react'
|
||||
import { Bot, CheckCircle2, Clock, Cpu, Inbox, Loader2, Server, Settings, XCircle } from 'lucide-react'
|
||||
import type { AutoRepairStats, RepairHistoryItem, UriScheme, RepairStatus, ActiveIncident } from './types'
|
||||
|
||||
interface Props {
|
||||
@@ -41,6 +41,13 @@ const NODE_STATE_STYLES = {
|
||||
waiting: 'border-border opacity-40',
|
||||
}
|
||||
|
||||
const SEVERITY_DOT_STYLES: Record<string, string> = {
|
||||
P0: 'bg-red-500 ring-red-500/20',
|
||||
P1: 'bg-orange-500 ring-orange-500/20',
|
||||
P2: 'bg-yellow-500 ring-yellow-500/20',
|
||||
P3: 'bg-green-500 ring-green-500/20',
|
||||
}
|
||||
|
||||
// 從最新一筆 history 衍生鏈路狀態
|
||||
function deriveChainNodes(latestItem: RepairHistoryItem | null, t: ReturnType<typeof useTranslations>) {
|
||||
if (!latestItem) {
|
||||
@@ -72,14 +79,8 @@ function computeAvgDuration(history: RepairHistoryItem[]): string {
|
||||
return `${(avg / 1000).toFixed(1)}s`
|
||||
}
|
||||
|
||||
// 嚴重度 → emoji 對應
|
||||
function severityEmoji(severity: string): string {
|
||||
switch (severity) {
|
||||
case 'P0': return '🔴'
|
||||
case 'P1': return '🟠'
|
||||
case 'P2': return '🟡'
|
||||
default: return '🟢'
|
||||
}
|
||||
function severityDotClass(severity: string): string {
|
||||
return SEVERITY_DOT_STYLES[severity] ?? SEVERITY_DOT_STYLES.P3
|
||||
}
|
||||
|
||||
export function NeuralLiveCenter({ stats, history, pendingCount, activeIncidents = [] }: Props) {
|
||||
@@ -101,7 +102,9 @@ export function NeuralLiveCenter({ stats, history, pendingCount, activeIncidents
|
||||
{/* OpenClaw */}
|
||||
<div className="rounded-xl border border-orange-500/25 bg-orange-500/5 p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-orange-500/15 flex items-center justify-center text-base">🦞</div>
|
||||
<div className="w-8 h-8 rounded-lg bg-orange-500/15 flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-orange-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-orange-500">OpenClaw</p>
|
||||
<p className="text-[10px] text-muted-foreground">{t('agentRoleOC')}</p>
|
||||
@@ -118,7 +121,9 @@ export function NeuralLiveCenter({ stats, history, pendingCount, activeIncidents
|
||||
{/* NemoTron */}
|
||||
<div className="rounded-xl border border-blue-500/25 bg-blue-500/5 p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-500/15 flex items-center justify-center text-base">⚡</div>
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-500/15 flex items-center justify-center">
|
||||
<Cpu className="w-4 h-4 text-blue-500" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-blue-500">NemoTron</p>
|
||||
<p className="text-[10px] text-muted-foreground">{t('agentRoleNemo')}</p>
|
||||
@@ -142,7 +147,11 @@ export function NeuralLiveCenter({ stats, history, pendingCount, activeIncidents
|
||||
'flex items-center gap-2 px-2 py-1.5 rounded-lg cursor-pointer transition-colors',
|
||||
i === 0 ? 'bg-orange-500/8 border border-orange-500/20' : 'hover:bg-muted',
|
||||
)}>
|
||||
<span className="text-xs">{severityEmoji(inc.severity)}</span>
|
||||
<span
|
||||
className={cn('h-2.5 w-2.5 rounded-full ring-4 flex-shrink-0', severityDotClass(inc.severity))}
|
||||
aria-label={inc.severity}
|
||||
title={inc.severity}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[11px] font-semibold truncate">{inc.incident_id}</p>
|
||||
<p className="text-[10px] text-muted-foreground truncate">{inc.affected_services.join(', ') || inc.status}</p>
|
||||
@@ -187,16 +196,19 @@ export function NeuralLiveCenter({ stats, history, pendingCount, activeIncidents
|
||||
<div className="w-0.5 h-4 bg-muted-foreground/20 rounded-full" />
|
||||
<div className="w-full max-w-sm grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ scheme: 'kubectl://', icon: '☸️', label: 'kubectl', cls: 'border-blue-500/25 bg-blue-500/5' },
|
||||
{ scheme: 'openclaw://', icon: '🦞', label: 'OpenClaw', cls: 'border-orange-500/25 bg-orange-500/5' },
|
||||
{ scheme: 'ansible://', icon: '⚙️', label: 'Ansible .188', cls: 'border-purple-500/25 bg-purple-500/5' },
|
||||
].map(b => (
|
||||
<div key={b.scheme} className={cn('rounded-lg border p-2.5 text-center opacity-50', b.cls)}>
|
||||
<p className="text-sm">{b.icon}</p>
|
||||
<p className="text-[10px] font-bold mt-1">{b.label}</p>
|
||||
<p className="text-[9px] text-muted-foreground font-mono">{b.scheme}</p>
|
||||
</div>
|
||||
))}
|
||||
{ scheme: 'kubectl://', Icon: Server, label: 'kubectl', cls: 'border-blue-500/25 bg-blue-500/5', iconCls: 'text-blue-500' },
|
||||
{ scheme: 'openclaw://', Icon: Bot, label: 'OpenClaw', cls: 'border-orange-500/25 bg-orange-500/5', iconCls: 'text-orange-500' },
|
||||
{ scheme: 'ansible://', Icon: Settings, label: 'Ansible .188', cls: 'border-purple-500/25 bg-purple-500/5', iconCls: 'text-purple-500' },
|
||||
].map(b => {
|
||||
const BranchIcon = b.Icon
|
||||
return (
|
||||
<div key={b.scheme} className={cn('rounded-lg border p-2.5 text-center opacity-70', b.cls)}>
|
||||
<BranchIcon className={cn('mx-auto h-4 w-4', b.iconCls)} aria-hidden="true" />
|
||||
<p className="text-[10px] font-bold mt-1">{b.label}</p>
|
||||
<p className="text-[9px] text-muted-foreground font-mono">{b.scheme}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Inbox } from 'lucide-react'
|
||||
import { Bot, Inbox, Server, Settings } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { AutoRepairStats, PlaybookItem, RepairHistoryItem } from './types'
|
||||
|
||||
interface DispositionSummary {
|
||||
@@ -38,10 +39,10 @@ const TYPE_BADGE: Record<string, string> = {
|
||||
openclaw: 'bg-orange-500/10 text-orange-500 border border-orange-500/25',
|
||||
}
|
||||
|
||||
const SCHEME_ICON: Record<string, { icon: string; color: string; textColor: string; bg: string }> = {
|
||||
'kubectl://': { icon: '☸️', color: 'bg-blue-500', textColor: 'text-blue-500', bg: 'bg-blue-500/10' },
|
||||
'openclaw://': { icon: '🦞', color: 'bg-orange-500', textColor: 'text-orange-500', bg: 'bg-orange-500/10' },
|
||||
'ansible://': { icon: '⚙️', color: 'bg-purple-500', textColor: 'text-purple-500', bg: 'bg-purple-500/10' },
|
||||
const SCHEME_STYLE: Record<string, { Icon: LucideIcon; color: string; textColor: string; bg: string }> = {
|
||||
'kubectl://': { Icon: Server, color: 'bg-blue-500', textColor: 'text-blue-500', bg: 'bg-blue-500/10' },
|
||||
'openclaw://': { Icon: Bot, color: 'bg-orange-500', textColor: 'text-orange-500', bg: 'bg-orange-500/10' },
|
||||
'ansible://': { Icon: Settings, color: 'bg-purple-500', textColor: 'text-purple-500', bg: 'bg-purple-500/10' },
|
||||
}
|
||||
|
||||
export function NeuralStats({ stats, playbooks, history, pendingCount, disposition }: Props) {
|
||||
@@ -59,7 +60,7 @@ export function NeuralStats({ stats, playbooks, history, pendingCount, dispositi
|
||||
const total = history.length || 1
|
||||
return Object.entries(counts).map(([scheme, { count, successCount }]) => ({
|
||||
scheme,
|
||||
...(SCHEME_ICON[scheme] ?? SCHEME_ICON['kubectl://']),
|
||||
...(SCHEME_STYLE[scheme] ?? SCHEME_STYLE['kubectl://']),
|
||||
count,
|
||||
rate: count > 0 ? Math.round((successCount / count) * 100) : 0,
|
||||
pct: Math.round((count / total) * 100),
|
||||
@@ -145,9 +146,13 @@ export function NeuralStats({ stats, playbooks, history, pendingCount, dispositi
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-4">{t('schemeBreakdown')}</p>
|
||||
{schemeStats.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{schemeStats.map(s => (
|
||||
{schemeStats.map(s => {
|
||||
const SchemeIcon = s.Icon
|
||||
return (
|
||||
<div key={s.scheme} className="flex items-center gap-3">
|
||||
<span className="text-sm w-5 text-center">{s.icon}</span>
|
||||
<span className={cn('flex h-6 w-6 items-center justify-center rounded-md', s.bg)}>
|
||||
<SchemeIcon className={cn('h-3.5 w-3.5', s.textColor)} aria-hidden="true" />
|
||||
</span>
|
||||
<span className={cn('text-xs font-semibold w-24 font-mono', s.textColor)}>{s.scheme}</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div className={cn('h-full rounded-full', s.color)} style={{ width: `${s.pct}%` }} />
|
||||
@@ -155,7 +160,8 @@ export function NeuralStats({ stats, playbooks, history, pendingCount, dispositi
|
||||
<span className={cn('text-xs font-bold w-6 text-right', s.textColor)}>{s.count}</span>
|
||||
<span className="text-xs text-muted-foreground w-8 text-right">{s.rate}%</span>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user