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:
85
src/pages/agent-office/AgentOffice.jsx
Normal file
85
src/pages/agent-office/AgentOffice.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user