feat(governance): 新增 AI Agent 活動動畫
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m30s
CD Pipeline / build-and-deploy (push) Successful in 4m3s
CD Pipeline / post-deploy-checks (push) Successful in 1m59s

This commit is contained in:
Your Name
2026-06-14 09:42:37 +08:00
parent f2fa845464
commit a3de0ffb82
6 changed files with 570 additions and 0 deletions

View File

@@ -0,0 +1,472 @@
'use client'
import { BrainCircuit, Cpu, RadioTower, Route, ShieldCheck } from 'lucide-react'
import type { CSSProperties } from 'react'
import { useTranslations } from 'next-intl'
import { GlassCard } from '@/components/ui/glass-card'
type AgentActivityMode = 'automation' | 'market'
type AgentActivityTone = 'ok' | 'warn' | 'danger' | 'neutral'
export type AgentActivityMetric = {
label: string
value: string | number
tone?: AgentActivityTone
}
export type AgentActivityConstellationProps = {
mode: AgentActivityMode
statusValue: string
metrics: AgentActivityMetric[]
}
function toneColor(tone: AgentActivityTone = 'neutral') {
if (tone === 'ok') return '#22C55E'
if (tone === 'warn') return '#F59E0B'
if (tone === 'danger') return '#EF4444'
return '#4A90D9'
}
export function AgentActivityConstellation({
mode,
statusValue,
metrics,
}: AgentActivityConstellationProps) {
const t = useTranslations('governance.agentActivity')
const isMarket = mode === 'market'
const title = isMarket ? t('marketTitle') : t('automationTitle')
const subtitle = isMarket ? t('marketSubtitle') : t('automationSubtitle')
return (
<GlassCard variant="subtle" padding="md" className="agent-activity-constellation min-w-0">
<div className="agent-activity-layout">
<div
className={`agent-activity-stage ${isMarket ? 'is-market' : 'is-automation'}`}
aria-label={title}
role="img"
>
<div className="agent-activity-orbit agent-activity-orbit-outer" />
<div className="agent-activity-orbit agent-activity-orbit-inner" />
<div className="agent-activity-bus">
<RadioTower size={18} strokeWidth={1.8} />
</div>
<div className="agent-activity-node agent-activity-node-openclaw">
<ShieldCheck size={17} strokeWidth={1.9} />
<span>OpenClaw</span>
</div>
<div className="agent-activity-node agent-activity-node-hermes">
<BrainCircuit size={17} strokeWidth={1.9} />
<span>Hermes</span>
</div>
<div className="agent-activity-node agent-activity-node-nemotron">
<Cpu size={17} strokeWidth={1.9} />
<span>NemoTron</span>
</div>
<div className="agent-activity-packet agent-activity-packet-a" />
<div className="agent-activity-packet agent-activity-packet-b" />
<div className="agent-activity-packet agent-activity-packet-c" />
</div>
<div className="agent-activity-copy">
<div className="agent-activity-heading">
<Route size={14} strokeWidth={1.8} />
<span>{title}</span>
</div>
<p>{subtitle}</p>
<div className="agent-activity-status">
<span>{t('status')}</span>
<strong>{statusValue}</strong>
</div>
<div className="agent-activity-metrics">
{metrics.map((metric) => (
<div
key={`${metric.label}-${metric.value}`}
className="agent-activity-metric"
style={{ '--metric-color': toneColor(metric.tone) } as CSSProperties}
>
<span>{metric.label}</span>
<strong>{metric.value}</strong>
</div>
))}
</div>
<div className="agent-activity-footnote">{t('footnote')}</div>
</div>
</div>
<style jsx>{`
.agent-activity-layout {
display: grid;
grid-template-columns: minmax(220px, 0.82fr) minmax(0, 1.18fr);
gap: 16px;
align-items: center;
min-width: 0;
}
.agent-activity-stage {
position: relative;
min-height: 190px;
border-radius: 8px;
border: 0.5px solid rgba(20, 20, 19, 0.06);
overflow: hidden;
background:
linear-gradient(90deg, rgba(20, 20, 19, 0.035) 1px, transparent 1px),
linear-gradient(0deg, rgba(20, 20, 19, 0.035) 1px, transparent 1px),
radial-gradient(circle at 50% 50%, rgba(74, 144, 217, 0.13), transparent 56%),
#fbfaf5;
background-size: 28px 28px, 28px 28px, auto, auto;
isolation: isolate;
}
.agent-activity-stage::before {
content: '';
position: absolute;
inset: -40%;
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(74, 144, 217, 0.2) 62deg,
transparent 126deg,
rgba(34, 197, 94, 0.18) 206deg,
transparent 300deg
);
animation: agent-activity-sweep 9s linear infinite;
z-index: 0;
}
.agent-activity-stage.is-market::before {
background: conic-gradient(
from 0deg,
transparent 0deg,
rgba(245, 158, 11, 0.2) 62deg,
transparent 142deg,
rgba(74, 144, 217, 0.2) 230deg,
transparent 300deg
);
}
.agent-activity-orbit {
position: absolute;
left: 50%;
top: 50%;
border-radius: 999px;
border: 1px solid rgba(20, 20, 19, 0.08);
transform: translate(-50%, -50%);
z-index: 1;
}
.agent-activity-orbit-outer {
width: 172px;
height: 124px;
border-style: dashed;
animation: agent-activity-drift 12s linear infinite;
}
.agent-activity-orbit-inner {
width: 104px;
height: 72px;
border-color: rgba(74, 144, 217, 0.24);
animation: agent-activity-drift-reverse 10s linear infinite;
}
.agent-activity-bus {
position: absolute;
left: 50%;
top: 50%;
width: 46px;
height: 46px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: #141413;
background: rgba(255, 255, 255, 0.86);
border: 0.5px solid rgba(20, 20, 19, 0.08);
box-shadow: 0 12px 26px rgba(20, 20, 19, 0.08);
transform: translate(-50%, -50%);
z-index: 3;
}
.agent-activity-node {
position: absolute;
z-index: 4;
display: inline-flex;
align-items: center;
gap: 6px;
max-width: calc(100% - 24px);
min-height: 30px;
padding: 5px 8px;
border-radius: 7px;
border: 0.5px solid rgba(20, 20, 19, 0.08);
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10px 22px rgba(20, 20, 19, 0.06);
color: #141413;
font-family: 'DM Mono', monospace;
font-size: 10px;
line-height: 1.15;
white-space: nowrap;
animation: agent-activity-node-pulse 3.6s ease-in-out infinite;
}
.agent-activity-node-openclaw {
left: 16px;
top: 20px;
color: #4a90d9;
animation-delay: 0s;
}
.agent-activity-node-hermes {
right: 14px;
top: 56px;
color: #d97757;
animation-delay: 0.5s;
}
.agent-activity-node-nemotron {
left: 50%;
bottom: 18px;
color: #22c55e;
transform: translateX(-50%);
animation-delay: 1s;
}
.agent-activity-packet {
position: absolute;
width: 7px;
height: 7px;
border-radius: 999px;
background: #4a90d9;
box-shadow: 0 0 18px rgba(74, 144, 217, 0.55);
z-index: 2;
}
.agent-activity-packet-a {
animation: agent-activity-packet-a 4.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.agent-activity-packet-b {
background: #d97757;
box-shadow: 0 0 18px rgba(217, 119, 87, 0.5);
animation: agent-activity-packet-b 5.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.agent-activity-packet-c {
background: #22c55e;
box-shadow: 0 0 18px rgba(34, 197, 94, 0.5);
animation: agent-activity-packet-c 6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.agent-activity-copy {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.agent-activity-heading {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
color: #d97757;
}
.agent-activity-heading span {
color: #141413;
font-family: Syne, sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 1.25;
min-width: 0;
overflow-wrap: anywhere;
}
.agent-activity-copy p {
margin: 0;
color: #625f58;
font-family: 'DM Mono', monospace;
font-size: 11px;
line-height: 1.55;
overflow-wrap: anywhere;
}
.agent-activity-status {
display: inline-flex;
align-items: center;
gap: 7px;
max-width: 100%;
min-width: 0;
color: #87867f;
font-family: 'DM Mono', monospace;
font-size: 10px;
overflow-wrap: anywhere;
}
.agent-activity-status strong {
color: #141413;
font-size: 11px;
font-weight: 700;
min-width: 0;
overflow-wrap: anywhere;
}
.agent-activity-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
min-width: 0;
}
.agent-activity-metric {
min-width: 0;
border-left: 2px solid var(--metric-color);
padding: 7px 8px;
border-radius: 6px;
background: rgba(250, 249, 243, 0.74);
}
.agent-activity-metric span,
.agent-activity-footnote {
display: block;
color: #87867f;
font-family: 'DM Mono', monospace;
font-size: 9px;
line-height: 1.3;
overflow-wrap: anywhere;
}
.agent-activity-metric strong {
display: block;
margin-top: 3px;
color: #141413;
font-family: 'DM Mono', monospace;
font-size: 13px;
line-height: 1.2;
overflow-wrap: anywhere;
}
.agent-activity-footnote {
padding-top: 2px;
}
@keyframes agent-activity-sweep {
to {
transform: rotate(360deg);
}
}
@keyframes agent-activity-drift {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes agent-activity-drift-reverse {
to {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@keyframes agent-activity-node-pulse {
0%,
100% {
box-shadow: 0 10px 22px rgba(20, 20, 19, 0.06);
}
50% {
box-shadow: 0 12px 30px rgba(74, 144, 217, 0.18);
}
}
@keyframes agent-activity-packet-a {
0%,
100% {
left: 52px;
top: 46px;
opacity: 0.2;
}
42% {
left: 50%;
top: 50%;
opacity: 1;
}
72% {
left: calc(100% - 58px);
top: 76px;
opacity: 0.75;
}
}
@keyframes agent-activity-packet-b {
0%,
100% {
right: 58px;
top: 82px;
opacity: 0.18;
}
42% {
right: 50%;
top: 50%;
opacity: 1;
}
72% {
right: calc(50% - 6px);
top: calc(100% - 45px);
opacity: 0.78;
}
}
@keyframes agent-activity-packet-c {
0%,
100% {
left: 50%;
bottom: 38px;
opacity: 0.18;
}
42% {
left: calc(50% - 2px);
bottom: 50%;
opacity: 1;
}
72% {
left: 54px;
bottom: calc(100% - 54px);
opacity: 0.72;
}
}
@media (max-width: 900px) {
.agent-activity-layout {
grid-template-columns: 1fr;
}
.agent-activity-stage {
min-height: 176px;
}
}
@media (max-width: 640px) {
.agent-activity-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.agent-activity-node {
font-size: 9px;
padding: 5px 7px;
}
}
@media (prefers-reduced-motion: reduce) {
.agent-activity-stage::before,
.agent-activity-orbit,
.agent-activity-node,
.agent-activity-packet {
animation: none;
}
.agent-activity-packet {
opacity: 0.65;
}
}
`}</style>
</GlassCard>
)
}