feat: Agent Office — AI 에이전트 가상 오피스 (#2)

## Summary
- Canvas 2D 픽셀아트 오피스 렌더링 (SpriteSheet + TileMap + AgentSprite)
- WebSocket 실시간 에이전트 상태 동기화 (useAgentManager)
- ChatPanel (명령/승인) + TaskHistory (작업 이력) UI
- 다크 테마 + glassmorphism 패널

## Changes (7 commits)
- API helpers + route + Lab entry
- Canvas engine: SpriteSheet, TileMap, AgentSprite, OfficeRenderer
- React hooks: useAgentManager, useOfficeCanvas
- Components: ChatPanel, TaskHistory
- Main page + CSS
- Code review fixes: claude agent 참조 제거, rejected 배지 추가

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-04-11 13:35:35 +09:00
parent 7fc2d3aaf7
commit 25715a2198
14 changed files with 1206 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import { useAgentManager } from './hooks/useAgentManager';
import { useOfficeCanvas } from './hooks/useOfficeCanvas';
import ChatPanel from './components/ChatPanel';
import TaskHistory from './components/TaskHistory';
import './AgentOffice.css';
export function Component() {
const canvasContainerRef = useRef(null);
const [selectedAgent, setSelectedAgent] = useState(null);
const [showHistory, setShowHistory] = useState(null);
const { agents, pendingTasks, connected, sendCommand, sendApproval } = useAgentManager();
const handleAgentClick = useCallback((agentId) => {
setSelectedAgent(prev => prev === agentId ? null : agentId);
}, []);
const { updateAgentState, moveAgent } = useOfficeCanvas(canvasContainerRef, handleAgentClick);
useEffect(() => {
for (const [id, info] of Object.entries(agents)) {
updateAgentState(id, info.state, info.detail);
}
}, [agents, updateAgentState]);
return (
<div className="ao-page">
<div className="ao-header">
<h1 className="ao-title">Agent Office</h1>
<div className="ao-status">
<span className={`ao-dot ${connected ? 'ao-dot--on' : 'ao-dot--off'}`} />
{connected ? 'Connected' : 'Disconnected'}
</div>
</div>
<div className="ao-workspace">
<div className="ao-canvas-container" ref={canvasContainerRef} />
<div className="ao-agent-bar">
{Object.entries(agents).map(([id, info]) => (
<button
key={id}
className={`ao-agent-chip ${info.state === 'waiting' ? 'ao-agent-chip--alert' : ''} ${selectedAgent === id ? 'ao-agent-chip--selected' : ''}`}
onClick={() => handleAgentClick(id)}
>
<span className={`ao-chip-dot ao-chip-dot--${info.state}`} />
{id}
{info.state === 'waiting' && <span className="ao-chip-badge">!</span>}
</button>
))}
{pendingTasks.length > 0 && (
<span className="ao-pending-count">{pendingTasks.length} pending</span>
)}
</div>
{selectedAgent && (
<ChatPanel
agentId={selectedAgent}
agentState={agents[selectedAgent]}
onCommand={sendCommand}
onApproval={sendApproval}
onClose={() => setSelectedAgent(null)}
/>
)}
{showHistory && (
<TaskHistory
agentId={showHistory}
onClose={() => setShowHistory(null)}
/>
)}
</div>
<div className="ao-toolbar">
{Object.keys(agents).map(id => (
<button key={id} className="ao-tool-btn"
onClick={() => setShowHistory(prev => prev === id ? null : id)}>
📋 {id} 이력
</button>
))}
</div>
</div>
);
}