mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
feat(pixel-office): add gateway-driven server alarm lamp and rack sprite
This commit is contained in:
@@ -173,6 +173,7 @@ const CODE_SNIPPET_LIFETIME_SEC = 5.5
|
||||
const FLOATING_TICK_INTERVAL_DESKTOP_MS = 48
|
||||
const FLOATING_TICK_INTERVAL_MOBILE_MS = 32
|
||||
const AGENT_ACTIVITY_POLL_INTERVAL_MS = 1000
|
||||
const GATEWAY_HEALTH_POLL_INTERVAL_MS = 10000
|
||||
|
||||
let cachedOfficeState: OfficeState | null = null
|
||||
let cachedEditorState: EditorState | null = null
|
||||
@@ -206,6 +207,7 @@ export default function PixelOfficePage() {
|
||||
const contributionsRef = useRef<ContributionData | null>(null)
|
||||
const photographRef = useRef<HTMLImageElement | null>(null)
|
||||
const gatewayRef = useRef<{ port: number; token?: string; host?: string }>({ port: 18789 })
|
||||
const gatewayHealthyRef = useRef<boolean>(true)
|
||||
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(cachedIsEditMode)
|
||||
const [soundOn, setSoundOn] = useState(true)
|
||||
@@ -425,7 +427,8 @@ export default function PixelOfficePage() {
|
||||
{ selectedAgentId: null, hoveredAgentId, hoveredTile: null, seats: office.seats, characters: office.characters },
|
||||
editorRender, office.layout.tileColors, office.layout.cols, office.layout.rows,
|
||||
undefined,
|
||||
contributionsRef.current ?? undefined, photographRef.current ?? undefined)
|
||||
contributionsRef.current ?? undefined, photographRef.current ?? undefined,
|
||||
gatewayHealthyRef.current)
|
||||
|
||||
// Collect photo comment positions for DOM rendering
|
||||
const zoom = zoomRef.current
|
||||
@@ -641,6 +644,22 @@ export default function PixelOfficePage() {
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Poll gateway health for server alarm lamp in Pixel Office
|
||||
useEffect(() => {
|
||||
const fetchGatewayHealth = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/gateway-health', { cache: 'no-store' })
|
||||
const data = await res.json()
|
||||
gatewayHealthyRef.current = !!data?.ok
|
||||
} catch {
|
||||
gatewayHealthyRef.current = false
|
||||
}
|
||||
}
|
||||
fetchGatewayHealth()
|
||||
const interval = setInterval(fetchGatewayHealth, GATEWAY_HEALTH_POLL_INTERVAL_MS)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// ── Editor helpers ──────────────────────────────────────────
|
||||
const applyEdit = useCallback((newLayout: OfficeLayout) => {
|
||||
const office = officeRef.current
|
||||
|
||||
@@ -229,6 +229,7 @@ export function renderScene(
|
||||
hoveredAgentId: number | null,
|
||||
contributions?: ContributionData,
|
||||
photograph?: HTMLImageElement,
|
||||
gatewayHealthy?: boolean,
|
||||
): void {
|
||||
const drawables: ZDrawable[] = []
|
||||
const laptopSizeScale = 0.7
|
||||
@@ -309,6 +310,57 @@ export function renderScene(
|
||||
c.drawImage(cached, fx, fy)
|
||||
},
|
||||
})
|
||||
|
||||
// Server alarm beacon (top of the server rack in the lounge left wall).
|
||||
if (f.uid === 'server-b-left') {
|
||||
const healthy = gatewayHealthy !== false
|
||||
const now = Date.now()
|
||||
const healthyPulse = (Math.sin(now / 900) + 1) / 2
|
||||
const unhealthyPulse = (Math.sin(now / 120) + 1) / 2
|
||||
const blinkAlpha = healthy
|
||||
? (0.55 + healthyPulse * 0.4) // slow breathing blink
|
||||
: (0.15 + unhealthyPulse * 0.85) // fast urgent blink
|
||||
drawables.push({
|
||||
zY: f.zY + 0.15,
|
||||
draw: (c) => {
|
||||
const lampX = Math.round(fx + 15 * zoom)
|
||||
const lampTopY = Math.round(fy + 1 * zoom)
|
||||
const lampW = Math.max(3, Math.round(3.6 * zoom))
|
||||
const lampH = Math.max(2, Math.round(2.2 * zoom))
|
||||
const stemW = Math.max(1, Math.round(1.1 * zoom))
|
||||
const stemH = Math.max(1, Math.round(1.4 * zoom))
|
||||
const baseW = Math.max(2, Math.round(2.6 * zoom))
|
||||
const baseH = Math.max(1, Math.round(1.1 * zoom))
|
||||
const lampLeft = Math.round(lampX - lampW / 2)
|
||||
const stemLeft = Math.round(lampX - stemW / 2)
|
||||
const stemTop = lampTopY + lampH
|
||||
const baseLeft = Math.round(lampX - baseW / 2)
|
||||
const baseTop = stemTop + stemH
|
||||
c.save()
|
||||
c.globalAlpha = blinkAlpha
|
||||
// Lamp cover (pixel warning light, not a sphere)
|
||||
c.fillStyle = '#2B2F45'
|
||||
c.fillRect(lampLeft - 1, lampTopY - 1, lampW + 2, lampH + 2)
|
||||
c.fillStyle = healthy ? '#63E46F' : '#F25F5C'
|
||||
c.fillRect(lampLeft, lampTopY, lampW, lampH)
|
||||
// Lamp stem + base
|
||||
c.fillStyle = '#3A425E'
|
||||
c.fillRect(stemLeft, stemTop, stemW, stemH)
|
||||
c.fillRect(baseLeft, baseTop, baseW, baseH)
|
||||
// Pixel glow bands to keep a lamp-like look (avoid spherical aura)
|
||||
const glowOuter = Math.max(8, Math.round(8.4 * zoom))
|
||||
const glowMid = Math.max(5, Math.round(5.8 * zoom))
|
||||
const glowInner = Math.max(3, Math.round(3.4 * zoom))
|
||||
c.fillStyle = healthy ? 'rgba(99,228,111,0.14)' : 'rgba(242,95,92,0.2)'
|
||||
c.fillRect(lampX - glowOuter, lampTopY - glowOuter, glowOuter * 2, glowOuter * 2)
|
||||
c.fillStyle = healthy ? 'rgba(99,228,111,0.2)' : 'rgba(242,95,92,0.28)'
|
||||
c.fillRect(lampX - glowMid, lampTopY - glowMid, glowMid * 2, glowMid * 2)
|
||||
c.fillStyle = healthy ? 'rgba(99,228,111,0.28)' : 'rgba(242,95,92,0.35)'
|
||||
c.fillRect(lampX - glowInner, lampTopY - glowInner, glowInner * 2, glowInner * 2)
|
||||
c.restore()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,6 +975,7 @@ export function renderFrame(
|
||||
bugs?: BugEntity[],
|
||||
contributions?: ContributionData,
|
||||
photograph?: HTMLImageElement,
|
||||
gatewayHealthy?: boolean,
|
||||
): { offsetX: number; offsetY: number } {
|
||||
// Clear
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||
@@ -960,7 +1013,7 @@ export function renderFrame(
|
||||
// Draw walls + furniture + characters (z-sorted)
|
||||
const selectedId = selection?.selectedAgentId ?? null
|
||||
const hoveredId = selection?.hoveredAgentId ?? null
|
||||
renderScene(ctx, allFurniture, characters, offsetX, offsetY, zoom, selectedId, hoveredId, contributions, photograph)
|
||||
renderScene(ctx, allFurniture, characters, offsetX, offsetY, zoom, selectedId, hoveredId, contributions, photograph, gatewayHealthy)
|
||||
|
||||
// Speech bubbles (always on top of characters)
|
||||
renderBubbles(ctx, characters, offsetX, offsetY, zoom)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PC_BACK_SPRITE,
|
||||
CAMERA_SPRITE,
|
||||
LAMP_SPRITE,
|
||||
SERVER_RACK_SPRITE,
|
||||
} from '../sprites/spriteData'
|
||||
import {
|
||||
TS_TABLE_WOOD_SM_VERTICAL, TS_TABLE_WOOD_SM_HORIZONTAL,
|
||||
@@ -62,6 +63,7 @@ export const FURNITURE_CATALOG: CatalogEntryWithCategory[] = [
|
||||
{ type: FurnitureType.SOFA, label: 'Sofa', footprintW: 2, footprintH: 1, sprite: [], isDesk: false, category: 'decor', emoji: '🛋️', emojiScale: 5 },
|
||||
{ type: FurnitureType.COFFEE, label: 'Coffee', footprintW: 1, footprintH: 1, sprite: [], isDesk: false, category: 'decor', emoji: '☕', canPlaceOnSurfaces: true, emojiScale: 0.67 },
|
||||
{ type: FurnitureType.LAMP, label: 'Lamp', footprintW: 1, footprintH: 1, sprite: LAMP_SPRITE, isDesk: false, category: 'decor' },
|
||||
{ type: FurnitureType.SERVER_RACK, label: 'Server Rack', footprintW: 2, footprintH: 2, sprite: SERVER_RACK_SPRITE, isDesk: false, category: 'electronics' },
|
||||
|
||||
// ── Tileset — Desks ──
|
||||
{ type: FurnitureType.TABLE_WOOD_SM_VERTICAL, label: 'Wood Table Vertical', footprintW: 1, footprintH: 2, sprite: TS_TABLE_WOOD_SM_VERTICAL, isDesk: true, category: 'desks' },
|
||||
|
||||
@@ -236,6 +236,12 @@ const RIGHT_WALL_STOOLS: ReadonlyArray<PlacedFurniture> = [
|
||||
{ uid: 'stool-r7', type: FurnitureType.BENCH, col: 17, row: 6 },
|
||||
{ uid: 'stool-r8', type: FurnitureType.BENCH, col: 17, row: 7.5 },
|
||||
]
|
||||
const LEFT_WALL_SERVER: Readonly<PlacedFurniture> = {
|
||||
uid: 'server-b-left',
|
||||
type: FurnitureType.SERVER_RACK,
|
||||
col: 1,
|
||||
row: 12,
|
||||
}
|
||||
|
||||
function shouldRemoveRightOfficeLegacyItems(item: PlacedFurniture): boolean {
|
||||
if (item.uid.startsWith('stool-r')) return true
|
||||
@@ -253,6 +259,9 @@ function normalizeRightOfficeFurniture(furniture: PlacedFurniture[]): PlacedFurn
|
||||
const exists = next.some((item) => item.uid === stool.uid)
|
||||
if (!exists) next.push({ ...stool })
|
||||
}
|
||||
if (!next.some((item) => item.uid === LEFT_WALL_SERVER.uid)) {
|
||||
next.push({ ...LEFT_WALL_SERVER })
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -357,6 +366,7 @@ export function createDefaultLayout(): OfficeLayout {
|
||||
|
||||
// ── Bottom lounge / break area ──
|
||||
{ uid: 'fridge-b', type: FurnitureType.FRIDGE, col: 1, row: 9.5 },
|
||||
{ ...LEFT_WALL_SERVER },
|
||||
{ uid: 'water-cooler-b', type: FurnitureType.WATER_COOLER, col: 8, row: 9.5 },
|
||||
{ uid: 'deco-b', type: FurnitureType.DECO_3, col: 9, row: 9.5 },
|
||||
{ uid: 'plant-b1', type: FurnitureType.PLANT, col: 1, row: 15 },
|
||||
|
||||
@@ -167,6 +167,81 @@ export const COOLER_SPRITE: SpriteData = (() => {
|
||||
]
|
||||
})()
|
||||
|
||||
/** Server rack: 32x32 (2 tiles wide, 2 tiles tall) */
|
||||
export const SERVER_RACK_SPRITE: SpriteData = (() => {
|
||||
// Polished to match reference more closely:
|
||||
// dual towers, vented top cap, layered front panels, dense cyan/green LED rows.
|
||||
const BG = _
|
||||
const FRAME = '#2C324B'
|
||||
const EDGE = '#4B547E'
|
||||
const TOP = '#626A94'
|
||||
const PANEL = '#1B2238'
|
||||
const SLOT = '#0E1425'
|
||||
const SLOT_DIV = '#222B45'
|
||||
const CYAN = '#27D5FF'
|
||||
const CYAN_DIM = '#1B6B99'
|
||||
const GREEN = '#63E46F'
|
||||
const WHITE = '#DCE6F2'
|
||||
const BASE = '#11182A'
|
||||
|
||||
const W = 32
|
||||
const H = 32
|
||||
const rows: string[][] = Array.from({ length: H }, () => Array.from({ length: W }, () => BG))
|
||||
|
||||
const px = (x: number, y: number, c: string) => {
|
||||
if (x >= 0 && x < W && y >= 0 && y < H) rows[y][x] = c
|
||||
}
|
||||
const fill = (x: number, y: number, w: number, h: number, c: string) => {
|
||||
for (let yy = y; yy < y + h; yy++) for (let xx = x; xx < x + w; xx++) px(xx, yy, c)
|
||||
}
|
||||
|
||||
const drawRack = (x0: number) => {
|
||||
// Outer shell
|
||||
fill(x0, 3, 14, 28, FRAME)
|
||||
fill(x0 + 1, 4, 12, 25, EDGE)
|
||||
fill(x0 + 2, 5, 10, 23, PANEL)
|
||||
|
||||
// Top cap with vents + tiny status pixel
|
||||
fill(x0 + 2, 4, 10, 4, TOP)
|
||||
fill(x0 + 3, 5, 2, 1, SLOT_DIV)
|
||||
fill(x0 + 6, 5, 2, 1, SLOT_DIV)
|
||||
fill(x0 + 9, 5, 2, 1, SLOT_DIV)
|
||||
fill(x0 + 3, 6, 2, 1, SLOT)
|
||||
fill(x0 + 6, 6, 2, 1, SLOT)
|
||||
fill(x0 + 9, 6, 2, 1, SLOT)
|
||||
px(x0 + 3, 4, WHITE)
|
||||
|
||||
// Rack units (8 rows)
|
||||
let y = 9
|
||||
for (let u = 0; u < 8; u++) {
|
||||
fill(x0 + 2, y, 10, 2, SLOT)
|
||||
fill(x0 + 2, y, 10, 1, SLOT_DIV) // divider strip
|
||||
// LED bars
|
||||
px(x0 + 3, y + 1, CYAN)
|
||||
px(x0 + 4, y + 1, CYAN)
|
||||
px(x0 + 5, y + 1, CYAN_DIM)
|
||||
px(x0 + 7, y + 1, u % 2 === 0 ? CYAN : CYAN_DIM)
|
||||
px(x0 + 8, y + 1, u % 3 === 0 ? CYAN : CYAN_DIM)
|
||||
px(x0 + 10, y + 1, GREEN)
|
||||
y += 2
|
||||
}
|
||||
|
||||
// Bottom I/O + base
|
||||
fill(x0 + 2, 25, 10, 2, SLOT)
|
||||
px(x0 + 3, 26, CYAN_DIM)
|
||||
px(x0 + 5, 26, CYAN_DIM)
|
||||
px(x0 + 7, 26, CYAN_DIM)
|
||||
px(x0 + 9, 26, CYAN_DIM)
|
||||
fill(x0 + 1, 29, 12, 1, BASE)
|
||||
fill(x0 + 2, 30, 2, 1, BASE)
|
||||
fill(x0 + 10, 30, 2, 1, BASE)
|
||||
}
|
||||
|
||||
drawRack(1)
|
||||
drawRack(15)
|
||||
return rows
|
||||
})()
|
||||
|
||||
/** Whiteboard: 32x16 (2 tiles wide, 1 tile tall) — hangs on wall */
|
||||
export const WHITEBOARD_SPRITE: SpriteData = (() => {
|
||||
const F = '#AAAAAA'
|
||||
|
||||
@@ -117,6 +117,7 @@ export const FurnitureType = {
|
||||
PAINTING_SMALL_1: 'ts_painting_small_1',
|
||||
PAINTING_SMALL_2: 'ts_painting_small_2',
|
||||
PAINTING_SMALL_3: 'ts_painting_small_3',
|
||||
SERVER_RACK: 'server_rack',
|
||||
PHONE: 'phone',
|
||||
SOFA: 'sofa',
|
||||
COFFEE: 'coffee',
|
||||
|
||||
Reference in New Issue
Block a user