mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
fix(ui): stabilize pixel-office loop and cache dashboard views
This commit is contained in:
+45
-16
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user