fix(ui): stabilize pixel-office loop and cache dashboard views

This commit is contained in:
xmanrui
2026-03-01 11:26:59 +08:00
parent a106d8002a
commit fd722772b9
3 changed files with 130 additions and 30 deletions
+45 -16
View File
@@ -64,6 +64,13 @@ type TimeRange = "daily" | "weekly" | "monthly";
type TFunc = (key: string) => string;
let cachedHomeData: ConfigData | null = null;
let cachedHomeError: string | null = null;
let cachedHomeAllStats: AllStats | null = null;
let cachedHomeLastUpdated = "";
let cachedHomeRefreshInterval = 0;
let cachedHomeAgentStates: Record<string, string> = {};
function formatTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
@@ -488,12 +495,12 @@ function AgentCard({ agent, gatewayPort, gatewayToken, t, testResult, platformTe
export default function Home() {
const { t } = useI18n();
const [data, setData] = useState<ConfigData | null>(null);
const [error, setError] = useState<string | null>(null);
const [refreshInterval, setRefreshInterval] = useState(0);
const [lastUpdated, setLastUpdated] = useState<string>("");
const [data, setData] = useState<ConfigData | null>(cachedHomeData);
const [error, setError] = useState<string | null>(cachedHomeError);
const [refreshInterval, setRefreshInterval] = useState(cachedHomeRefreshInterval);
const [lastUpdated, setLastUpdated] = useState<string>(cachedHomeLastUpdated);
const [loading, setLoading] = useState(false);
const [allStats, setAllStats] = useState<AllStats | null>(null);
const [allStats, setAllStats] = useState<AllStats | null>(cachedHomeAllStats);
const [statsRange, setStatsRange] = useState<TimeRange>("daily");
const timerRef = useRef<NodeJS.Timeout | null>(null);
const [testResults, setTestResults] = useState<Record<string, { ok: boolean; text?: string; error?: string; elapsed: number }> | null>(null);
@@ -504,7 +511,7 @@ export default function Home() {
const [testingSessions, setTestingSessions] = useState(false);
const [dmSessionResults, setDmSessionResults] = useState<Record<string, PlatformTestResult | null> | null>(null);
const [testingDmSessions, setTestingDmSessions] = useState(false);
const [agentStates, setAgentStates] = useState<Record<string, string>>({});
const [agentStates, setAgentStates] = useState<Record<string, string>>(cachedHomeAgentStates);
const RANGE_LABELS: Record<TimeRange, string> = { daily: t("range.daily"), weekly: t("range.weekly"), monthly: t("range.monthly") };
@@ -517,25 +524,42 @@ export default function Home() {
{ label: t("refresh.10m"), value: 600 },
];
const fetchData = useCallback(() => {
setLoading(true);
const fetchData = useCallback((silent = false) => {
if (!silent) setLoading(true);
Promise.all([
fetch("/api/config").then((r) => r.json()),
fetch("/api/stats-all").then((r) => r.json()),
])
.then(([configData, statsData]) => {
if (configData.error) setError(configData.error);
else { setData(configData); setError(null); }
if (!statsData.error) setAllStats(statsData);
setLastUpdated(new Date().toLocaleTimeString("zh-CN"));
if (configData.error) {
setError(configData.error);
cachedHomeError = configData.error;
} else {
setData(configData);
setError(null);
cachedHomeData = configData;
cachedHomeError = null;
}
if (!statsData.error) {
setAllStats(statsData);
cachedHomeAllStats = statsData;
}
const updated = new Date().toLocaleTimeString("zh-CN");
setLastUpdated(updated);
cachedHomeLastUpdated = updated;
})
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
.catch((e) => {
setError(e.message);
cachedHomeError = e.message;
})
.finally(() => {
if (!silent) setLoading(false);
});
}, []);
// 首次加载 - 从 localStorage 恢复测试状态
useEffect(() => {
fetchData();
fetchData(!!cachedHomeData);
const savedTestResults = localStorage.getItem('agentTestResults');
if (savedTestResults) {
try {
@@ -570,6 +594,10 @@ export default function Home() {
}
}, [fetchData]);
useEffect(() => {
cachedHomeRefreshInterval = refreshInterval;
}, [refreshInterval]);
// 保存测试结果到 localStorage
useEffect(() => {
if (testResults) {
@@ -700,6 +728,7 @@ export default function Home() {
const map: Record<string, string> = {};
for (const s of d.statuses) map[s.agentId] = s.state;
setAgentStates(map);
cachedHomeAgentStates = map;
}
})
.catch(() => {});
@@ -790,7 +819,7 @@ export default function Home() {
</select>
{refreshInterval === 0 && (
<button
onClick={fetchData}
onClick={() => fetchData(false)}
disabled={loading}
className="px-3 py-2 rounded-lg bg-[var(--card)] border border-[var(--border)] text-sm hover:border-[var(--accent)] transition disabled:opacity-50"
>
+73 -9
View File
@@ -108,21 +108,32 @@ function getGhostBorderDirection(col: number, row: number, cols: number, rows: n
const FIXED_CANVAS_ZOOM = 2.5
let cachedOfficeState: OfficeState | null = null
let cachedEditorState: EditorState | null = null
let cachedSavedLayout: OfficeLayout | null = null
let cachedPan: { x: number; y: number } = { x: 0, y: 0 }
let cachedIsEditMode = false
let spriteAssetsPromise: Promise<void> | null = null
let cachedAgents: AgentActivity[] = []
let cachedAgentIdMap = new Map<string, number>()
let cachedNextCharacterId = 1
let cachedPrevAgentStates = new Map<string, string>()
export default function PixelOfficePage() {
const { t } = useI18n()
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const officeRef = useRef<OfficeState | null>(null)
const editorRef = useRef<EditorState>(new EditorState())
const agentIdMapRef = useRef<Map<string, number>>(new Map())
const nextIdRef = useRef<{ current: number }>({ current: 1 })
const editorRef = useRef<EditorState>(cachedEditorState ?? new EditorState())
const agentIdMapRef = useRef<Map<string, number>>(new Map(cachedAgentIdMap))
const nextIdRef = useRef<{ current: number }>({ current: cachedNextCharacterId })
const zoomRef = useRef<number>(FIXED_CANVAS_ZOOM)
const panRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const savedLayoutRef = useRef<OfficeLayout | null>(null)
const panRef = useRef<{ x: number; y: number }>(cachedPan)
const savedLayoutRef = useRef<OfficeLayout | null>(cachedSavedLayout)
const animationFrameIdRef = useRef<number | null>(null)
const prevAgentStatesRef = useRef<Map<string, string>>(new Map())
const prevAgentStatesRef = useRef<Map<string, string>>(new Map(cachedPrevAgentStates))
const [agents, setAgents] = useState<AgentActivity[]>([])
const [agents, setAgents] = useState<AgentActivity[]>(cachedAgents)
const [hoveredAgentId, setHoveredAgentId] = useState<number | null>(null)
const mousePosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const agentStatsRef = useRef<Map<string, { sessionCount: number; messageCount: number; totalTokens: number; todayAvgResponseMs: number; weeklyResponseMs: number[]; weeklyTokens: number[]; lastActive: number | null }>>(new Map())
@@ -130,7 +141,7 @@ export default function PixelOfficePage() {
const photographRef = useRef<HTMLImageElement | null>(null)
const gatewayRef = useRef<{ port: number; token?: string }>({ port: 18789 })
const providersRef = useRef<Array<{ id: string; api: string; models: Array<{ id: string; name: string; contextWindow?: number }>; usedBy: Array<{ id: string; emoji: string; name: string }> }>>([])
const [isEditMode, setIsEditMode] = useState(false)
const [isEditMode, setIsEditMode] = useState(cachedIsEditMode)
const [soundOn, setSoundOn] = useState(true)
const [editorTick, setEditorTick] = useState(0)
const [officeReady, setOfficeReady] = useState(false)
@@ -152,6 +163,19 @@ export default function PixelOfficePage() {
// Load saved layout and sound preference
useEffect(() => {
const loadLayout = async () => {
if (cachedOfficeState) {
officeRef.current = cachedOfficeState
savedLayoutRef.current = cachedSavedLayout
editorRef.current = cachedEditorState ?? editorRef.current
panRef.current = cachedPan
setIsEditMode(cachedIsEditMode)
if (!spriteAssetsPromise) {
spriteAssetsPromise = Promise.all([loadCharacterPNGs(), loadWallPNG()]).then(() => undefined)
}
await spriteAssetsPromise
setOfficeReady(true)
return
}
try {
const res = await fetch('/api/pixel-office/layout')
const data = await res.json()
@@ -164,7 +188,12 @@ export default function PixelOfficePage() {
} catch {
officeRef.current = new OfficeState()
}
await Promise.all([loadCharacterPNGs(), loadWallPNG()])
cachedOfficeState = officeRef.current
cachedSavedLayout = savedLayoutRef.current
if (!spriteAssetsPromise) {
spriteAssetsPromise = Promise.all([loadCharacterPNGs(), loadWallPNG()]).then(() => undefined)
}
await spriteAssetsPromise
setOfficeReady(true)
}
loadLayout()
@@ -177,12 +206,30 @@ export default function PixelOfficePage() {
}
return () => {
cachedOfficeState = officeRef.current
cachedEditorState = editorRef.current
cachedSavedLayout = savedLayoutRef.current
cachedPan = panRef.current
cachedIsEditMode = editorRef.current.isEditMode
if (animationFrameIdRef.current !== null) {
cancelAnimationFrame(animationFrameIdRef.current)
}
}
}, [])
useEffect(() => {
cachedAgents = agents
}, [agents])
useEffect(() => {
cachedAgentIdMap = new Map(agentIdMapRef.current)
cachedNextCharacterId = nextIdRef.current.current
}, [agents])
useEffect(() => {
cachedPrevAgentStates = new Map(prevAgentStatesRef.current)
}, [agents])
useEffect(() => {
window.dispatchEvent(new CustomEvent('openclaw-logo-drag-start'))
return () => {
@@ -357,15 +404,24 @@ export default function PixelOfficePage() {
// Poll for agent activity + sound notification
useEffect(() => {
if (cachedAgents.length > 0) {
setAgents(cachedAgents)
if (officeRef.current) {
syncAgentsToOffice(cachedAgents, officeRef.current, agentIdMapRef.current, nextIdRef.current)
}
}
const fetchAgents = async () => {
try {
const res = await fetch('/api/agent-activity')
const data = await res.json()
const newAgents: AgentActivity[] = data.agents || []
setAgents(newAgents)
cachedAgents = newAgents
if (officeRef.current) {
syncAgentsToOffice(newAgents, officeRef.current, agentIdMapRef.current, nextIdRef.current)
cachedAgentIdMap = new Map(agentIdMapRef.current)
cachedNextCharacterId = nextIdRef.current.current
}
// Play sound when agent transitions to waiting
@@ -390,6 +446,7 @@ export default function PixelOfficePage() {
const stateMap = new Map<string, string>()
for (const a of newAgents) stateMap.set(a.agentId, a.state)
prevAgentStatesRef.current = stateMap
cachedPrevAgentStates = new Map(stateMap)
} catch (e) {
console.error('Failed to fetch agents:', e)
}
@@ -1046,6 +1103,13 @@ export default function PixelOfficePage() {
onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}
onContextMenu={handleContextMenu}
className="w-full h-full" />
{!officeReady && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-[#1a1a2e]/85 pointer-events-none">
<div className="px-4 py-2 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm text-[var(--text-muted)]">
...
</div>
</div>
)}
{/* Broadcast notifications */}
{broadcasts.length > 0 && (
+12 -5
View File
@@ -100,8 +100,6 @@ export function Sidebar() {
window.dispatchEvent(new CustomEvent("openclaw-bugs-config-change"));
};
const isPixelOfficePage = pathname.startsWith("/pixel-office");
return (
<>
<aside
@@ -116,7 +114,7 @@ export function Sidebar() {
<span
className="relative inline-block transition-opacity duration-300"
style={{
fontSize: "3.375rem",
fontSize: "4.219rem",
lineHeight: 1,
transform: `translate(${logoCarry.dx}px, ${logoCarry.dy}px) rotate(${logoCarry.angle}rad)`,
opacity: logoCarry.hidden ? 0 : 1,
@@ -140,7 +138,7 @@ export function Sidebar() {
<span
className="relative inline-block transition-opacity duration-300"
style={{
fontSize: "3.375rem",
fontSize: "4.219rem",
lineHeight: 1,
transform: `translate(${logoCarry.dx}px, ${logoCarry.dy}px) rotate(${logoCarry.angle}rad)`,
opacity: logoCarry.hidden ? 0 : 1,
@@ -207,7 +205,7 @@ export function Sidebar() {
</div>
</div>
))}
{!collapsed && isPixelOfficePage && (
{!collapsed && (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)]/65 p-1">
<button
onClick={() => setExperimentOpen((v) => !v)}
@@ -257,6 +255,15 @@ export function Sidebar() {
)}
</div>
)}
{collapsed && (
<button
onClick={() => setCollapsed(false)}
title="实验功能"
className="w-full flex items-center justify-center rounded-lg px-2 py-2 text-base border border-[var(--border)] bg-[var(--card)]/65 text-[var(--text)] hover:bg-[var(--bg)] transition-colors"
>
🧪
</button>
)}
</div>
</nav>
</aside>