feat(web): enrich OpenClaw live ops room animation
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 24s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-30 07:49:56 +08:00
parent 5006132024
commit 24276ea926
3 changed files with 132 additions and 7 deletions

View File

@@ -1357,6 +1357,11 @@
"marker": "marker",
"updated": "updated"
},
"animation": {
"loop": "loop",
"on": "on",
"off": "off"
},
"panels": {
"rollups": "Scene metrics",
"boundaries": "Safety boundaries",

View File

@@ -1357,6 +1357,11 @@
"marker": "部署 marker",
"updated": "更新"
},
"animation": {
"loop": "動畫迴圈",
"on": "開啟",
"off": "關閉"
},
"panels": {
"rollups": "場景指標",
"boundaries": "安全邊界",

View File

@@ -190,6 +190,11 @@ export default function OpenClawLiveOpsSpacePage({
"host_or_k8s_write_performed",
].every((key) => scene.boundaries[key] === false);
}, [scene]);
const packetSlots = useMemo(() => {
const count = Math.max(8, Math.min(14, scene?.rollups.animated_entity_count ?? 10));
return Array.from({ length: count }, (_, index) => index);
}, [scene]);
const animationLoopEnabled = scene?.room.animation_loop.enabled === true;
return (
<AppLayout locale={params.locale} fullBleed>
@@ -238,16 +243,53 @@ export default function OpenClawLiveOpsSpacePage({
</div>
</div>
<div className="relative aspect-[16/10] min-h-[360px] overflow-hidden bg-[#eef3f8] sm:min-h-[500px]">
<div className="absolute inset-x-[5%] bottom-[7%] top-[8%] rotate-[-2deg] skew-y-[-7deg] border border-[#c8d6df] bg-[#fdfefe] shadow-[0_28px_70px_rgba(54,72,88,0.16)]" />
<div className="absolute inset-x-[7%] bottom-[11%] top-[12%] rotate-[-2deg] skew-y-[-7deg] bg-[linear-gradient(90deg,rgba(74,144,217,0.12)_1px,transparent_1px),linear-gradient(0deg,rgba(74,144,217,0.1)_1px,transparent_1px)] bg-[size:42px_42px]" />
<div
data-testid="openclaw-live-ops-room"
className="relative aspect-[16/10] min-h-[420px] overflow-hidden bg-[#e8eff3] sm:min-h-[560px]"
>
<div className="absolute inset-x-[3%] bottom-[4%] top-[7%] rotate-[-2deg] skew-y-[-7deg] border border-[#b7c8d0] bg-[#fdfefe] shadow-[0_30px_80px_rgba(37,55,68,0.18)]" />
<div className="absolute inset-x-[5%] bottom-[9%] top-[11%] rotate-[-2deg] skew-y-[-7deg] bg-[linear-gradient(90deg,rgba(74,144,217,0.14)_1px,transparent_1px),linear-gradient(0deg,rgba(74,144,217,0.11)_1px,transparent_1px)] bg-[size:44px_44px]" />
<div className="absolute left-[8%] top-[10%] h-[13%] w-[84%] rotate-[-2deg] skew-y-[-7deg] border border-[#c6d1d5] bg-[#e9eee8]" />
<div className="absolute left-[14%] top-[14%] h-10 w-[17%] rotate-[-2deg] skew-y-[-7deg] border border-[#aebbc0] bg-[#26333a] shadow-[0_10px_22px_rgba(37,55,68,0.22)]">
<span className="openclaw-screen-sweep absolute inset-0" />
<span className="absolute left-2 top-2 h-1.5 w-14 bg-[#70d6a4]" />
<span className="absolute bottom-2 left-2 h-1.5 w-9 bg-[#f0bd69]" />
</div>
<div className="absolute right-[14%] top-[14%] h-10 w-[17%] rotate-[-2deg] skew-y-[-7deg] border border-[#aebbc0] bg-[#26333a] shadow-[0_10px_22px_rgba(37,55,68,0.22)]">
<span className="openclaw-screen-sweep absolute inset-0" />
<span className="absolute left-2 top-2 h-1.5 w-11 bg-[#8fc29a]" />
<span className="absolute bottom-2 left-2 h-1.5 w-16 bg-[#4a90d9]" />
</div>
<div className="absolute left-1/2 top-[48%] h-[19%] w-[30%] -translate-x-1/2 -translate-y-1/2 rotate-[-2deg] skew-y-[-7deg] border border-[#b7b0a5] bg-[#ead9bd] shadow-[0_22px_42px_rgba(63,49,34,0.16)]" />
<div className="absolute left-1/2 top-[46%] h-[10%] w-[20%] -translate-x-1/2 -translate-y-1/2 rotate-[-2deg] skew-y-[-7deg] border border-[#87939a] bg-[#27333a] shadow-[0_14px_28px_rgba(37,55,68,0.2)]">
<span className="absolute left-3 top-3 h-1.5 w-16 bg-[#4a90d9]" />
<span className="absolute bottom-3 right-3 h-1.5 w-12 bg-[#8fc29a]" />
</div>
<div className="absolute left-[25%] top-[77%] h-[9%] w-[15%] rotate-[-2deg] skew-y-[-7deg] border border-[#b7b0a5] bg-[#d6c8aa]" />
<div className="absolute right-[20%] top-[76%] h-[10%] w-[12%] rotate-[-2deg] skew-y-[-7deg] border border-[#b7b0a5] bg-[#d8e5df]" />
{packetSlots.map((slot) => (
<span
key={slot}
data-testid="openclaw-flow-packet"
className="openclaw-flow-packet absolute h-2.5 w-2.5 border border-[#f0bd69] bg-[#fff6c7] shadow-[0_0_18px_rgba(240,189,105,0.5)]"
style={{
left: `${14 + (slot % 7) * 11}%`,
top: `${28 + (slot % 4) * 12}%`,
animationDelay: `${slot * 0.34}s`,
animationDuration: `${5.2 + (slot % 4) * 0.5}s`,
}}
/>
))}
{zones.map((zone) => {
const Icon = zoneIconByKind[zone.kind] ?? Activity;
return (
<div
key={zone.zone_id}
className="absolute flex h-14 w-28 -translate-x-1/2 -translate-y-1/2 rotate-[-2deg] items-center gap-2 border border-[#cbd4d8] bg-white/90 px-2 shadow-[0_10px_24px_rgba(54,72,88,0.12)] backdrop-blur"
data-testid="openclaw-zone"
className="absolute flex h-14 w-28 -translate-x-1/2 -translate-y-1/2 rotate-[-2deg] items-center gap-2 border border-[#cbd4d8] bg-white/95 px-2 shadow-[0_10px_24px_rgba(54,72,88,0.12)] backdrop-blur"
style={{ left: `${zone.position.x}%`, top: `${zone.position.y}%` }}
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center border border-[#d8d3c7] bg-[#f7f8f7] text-[#4a90d9]">
@@ -263,6 +305,7 @@ export default function OpenClawLiveOpsSpacePage({
{workItems.map((item, index) => (
<div
key={item.work_item_id}
data-testid="openclaw-work-token"
className={cn(
"openclaw-work-token absolute h-8 w-20 -translate-x-1/2 -translate-y-1/2 border px-2 py-1 text-[10px] font-semibold shadow-[0_8px_20px_rgba(54,72,88,0.16)]",
stateClass(stateFromWorkItemStatus(item.status)),
@@ -280,6 +323,7 @@ export default function OpenClawLiveOpsSpacePage({
{agents.map((agent, index) => (
<div
key={agent.agent_id}
data-testid="openclaw-agent-avatar"
className="openclaw-agent absolute -translate-x-1/2 -translate-y-1/2"
style={{
left: `${agent.position.x}%`,
@@ -289,10 +333,22 @@ export default function OpenClawLiveOpsSpacePage({
>
<div
className={cn(
"flex h-14 w-14 items-center justify-center border-2 bg-white shadow-[0_16px_30px_rgba(54,72,88,0.2)]",
"relative flex h-14 w-14 items-center justify-center border-2 bg-white shadow-[0_16px_30px_rgba(54,72,88,0.2)]",
stateClass(agent.state),
)}
>
<span
className={cn(
"absolute -right-1 -top-1 h-3 w-3 border border-white",
agent.state === "blocked"
? "bg-[#d95f4f]"
: agent.state === "verified"
? "bg-[#4f9b62]"
: agent.state === "working"
? "bg-[#4a90d9]"
: "bg-[#f0bd69]",
)}
/>
<Bot className="h-6 w-6" aria-hidden="true" />
</div>
<div className="mt-1 max-w-28 truncate border border-[#d8d3c7] bg-white px-1.5 py-0.5 text-center text-[10px] font-semibold text-[#34302a] shadow-sm">
@@ -306,6 +362,19 @@ export default function OpenClawLiveOpsSpacePage({
<span className="border border-[#d8d3c7] bg-white/90 px-2 py-1 font-mono text-[11px] text-[#5f5b52]">
{t("source.marker")} {shortValue(scene?.source.deploy_readback_marker)}
</span>
<span
data-testid="openclaw-animation-loop"
className={cn(
"border px-2 py-1 font-mono text-[11px]",
animationLoopEnabled
? "border-[#8fc29a] bg-[#f0faf2] text-[#17602a]"
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
)}
>
{t("animation.loop")}{" "}
{animationLoopEnabled ? t("animation.on") : t("animation.off")} ·{" "}
{scene?.room.animation_loop.tick_ms ?? 0}ms
</span>
<span className="border border-[#d8d3c7] bg-white/90 px-2 py-1 font-mono text-[11px] text-[#5f5b52]">
{t("source.updated")}{" "}
{updatedAt
@@ -328,7 +397,7 @@ export default function OpenClawLiveOpsSpacePage({
</h2>
<Activity className="h-4 w-4 text-[#4a90d9]" aria-hidden="true" />
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
{[
["agents", scene?.rollups.agent_count],
["workItems", scene?.rollups.work_item_count],
@@ -454,9 +523,55 @@ export default function OpenClawLiveOpsSpacePage({
animation: openclaw-work-token-flow 6.2s ease-in-out infinite;
will-change: transform;
}
@keyframes openclaw-flow-packet {
0% {
transform: translate3d(-8px, 10px, 0) scale(0.76);
opacity: 0;
}
18% {
opacity: 1;
}
50% {
transform: translate3d(22px, -14px, 0) scale(1);
opacity: 0.86;
}
100% {
transform: translate3d(48px, -28px, 0) scale(0.72);
opacity: 0;
}
}
@keyframes openclaw-screen-sweep {
0% {
transform: translateX(-100%);
opacity: 0;
}
30% {
opacity: 0.42;
}
100% {
transform: translateX(160%);
opacity: 0;
}
}
.openclaw-flow-packet {
animation: openclaw-flow-packet 5.8s ease-in-out infinite;
will-change: transform, opacity;
}
.openclaw-screen-sweep {
background: linear-gradient(
90deg,
transparent,
rgba(112, 214, 164, 0.3),
transparent
);
animation: openclaw-screen-sweep 3.8s linear infinite;
will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) {
.openclaw-agent,
.openclaw-work-token {
.openclaw-work-token,
.openclaw-flow-packet,
.openclaw-screen-sweep {
animation: none;
}
}