refactor(agent-office): rewrite AgentOffice with full-screen canvas and side panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,110 +1,100 @@
|
|||||||
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
// src/pages/agent-office/AgentOffice.jsx
|
||||||
import { useAgentManager } from './hooks/useAgentManager';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useOfficeCanvas } from './hooks/useOfficeCanvas';
|
import { useAgentManager } from './hooks/useAgentManager.js';
|
||||||
import AgentColumn from './components/AgentColumn';
|
import { useOfficeCanvas } from './hooks/useOfficeCanvas.js';
|
||||||
import CommandColumn from './components/CommandColumn';
|
import TopBar from './components/TopBar.jsx';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
import SidePanel from './components/SidePanel.jsx';
|
||||||
import MobileSheet from '../../components/MobileSheet';
|
|
||||||
import './AgentOffice.css';
|
import './AgentOffice.css';
|
||||||
|
|
||||||
const AGENT_META = {
|
export default function AgentOffice() {
|
||||||
stock: { name: '주식 트레이더', color: '#4488cc' },
|
const {
|
||||||
music: { name: '음악 프로듀서', color: '#44aa88' },
|
agents, pendingTasks, notifications, connected,
|
||||||
blog: { name: '블로그 마케터', color: '#d97706' },
|
refreshTrigger, clearNotifications
|
||||||
realestate: { name: '청약 애널리스트', color: '#c026d3' },
|
} = useAgentManager();
|
||||||
};
|
|
||||||
|
|
||||||
const AGENT_IDS = ['stock', 'music', 'blog', 'realestate'];
|
const {
|
||||||
|
canvasRef, updateAgentState, setAgentNotification,
|
||||||
|
setTheme, setZoom, hitTest, getZoom
|
||||||
|
} = useOfficeCanvas();
|
||||||
|
|
||||||
export function Component() {
|
const [selectedAgent, setSelectedAgent] = useState(null);
|
||||||
const canvasContainerRef = useRef(null);
|
const [theme, setThemeState] = useState(localStorage.getItem('agent-office-theme') || 'modern');
|
||||||
const isMobile = useIsMobile();
|
const [zoom, setZoomState] = useState(2);
|
||||||
const [agentDetailSheet, setAgentDetailSheet] = useState(null); // agentId or null
|
|
||||||
|
|
||||||
const { agents, pendingTasks, connected, notifications, sendCommand, sendApproval, clearNotifications } = useAgentManager();
|
|
||||||
|
|
||||||
const handleAgentClick = useCallback((agentId) => {
|
|
||||||
clearNotifications(agentId);
|
|
||||||
if (isMobile) {
|
|
||||||
setAgentDetailSheet(agentId);
|
|
||||||
}
|
|
||||||
}, [clearNotifications, isMobile]);
|
|
||||||
|
|
||||||
const handleCeoClick = useCallback(() => {}, []);
|
|
||||||
|
|
||||||
const { updateAgentState, setAgentNotification, setCeoDocBadge } = useOfficeCanvas(canvasContainerRef, handleAgentClick, handleCeoClick);
|
|
||||||
|
|
||||||
|
// WebSocket 상태 → 캔버스 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const [id, info] of Object.entries(agents)) {
|
for (const [id, agentState] of Object.entries(agents)) {
|
||||||
updateAgentState(id, info.state, info.detail);
|
updateAgentState(id, agentState.state, agentState.detail);
|
||||||
}
|
}
|
||||||
}, [agents, updateAgentState]);
|
}, [agents, updateAgentState]);
|
||||||
|
|
||||||
|
// 알림 → 캔버스 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const [id, count] of Object.entries(notifications)) {
|
for (const [id, count] of Object.entries(notifications)) {
|
||||||
setAgentNotification(id, count);
|
setAgentNotification(id, count);
|
||||||
}
|
}
|
||||||
for (const id of Object.keys(agents)) {
|
}, [notifications, setAgentNotification]);
|
||||||
if (!notifications[id]) setAgentNotification(id, 0);
|
|
||||||
}
|
|
||||||
}, [notifications, agents, setAgentNotification]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// 캔버스 클릭 핸들러
|
||||||
const total = Object.values(notifications).reduce((s, n) => s + n, 0);
|
const handleCanvasClick = useCallback((e) => {
|
||||||
setCeoDocBadge(total);
|
const result = hitTest(e.clientX, e.clientY);
|
||||||
}, [notifications, setCeoDocBadge]);
|
if (result.type === 'agent') {
|
||||||
|
setSelectedAgent(result.id);
|
||||||
|
clearNotifications(result.id);
|
||||||
|
setAgentNotification(result.id, 0);
|
||||||
|
} else {
|
||||||
|
setSelectedAgent(null);
|
||||||
|
}
|
||||||
|
}, [hitTest, clearNotifications, setAgentNotification]);
|
||||||
|
|
||||||
|
// 테마 변경
|
||||||
|
const handleThemeChange = useCallback((name) => {
|
||||||
|
setThemeState(name);
|
||||||
|
setTheme(name);
|
||||||
|
}, [setTheme]);
|
||||||
|
|
||||||
|
// 줌 변경
|
||||||
|
const handleZoomChange = useCallback((level) => {
|
||||||
|
setZoomState(level);
|
||||||
|
setZoom(level);
|
||||||
|
}, [setZoom]);
|
||||||
|
|
||||||
|
// 선택된 에이전트의 pending task
|
||||||
|
const pendingTask = selectedAgent
|
||||||
|
? pendingTasks.find(t => t.agent_id === selectedAgent)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ao-page">
|
<div className="ao-root">
|
||||||
<div className="ao-header">
|
<TopBar
|
||||||
<h1 className="ao-title">Agent Office</h1>
|
connected={connected}
|
||||||
<div className="ao-status">
|
theme={theme}
|
||||||
<span className={`ao-dot ${connected ? 'ao-dot--on' : 'ao-dot--off'}`} />
|
onThemeChange={handleThemeChange}
|
||||||
{connected ? 'Connected' : 'Disconnected'}
|
zoom={zoom}
|
||||||
</div>
|
onZoomChange={handleZoomChange}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="ao-dashboard">
|
<div className="ao-main">
|
||||||
{AGENT_IDS.map(id => (
|
<canvas
|
||||||
<AgentColumn
|
ref={canvasRef}
|
||||||
key={id}
|
className="ao-canvas"
|
||||||
agentId={id}
|
onClick={handleCanvasClick}
|
||||||
meta={AGENT_META[id]}
|
|
||||||
agentState={agents[id]}
|
|
||||||
notification={notifications[id] || 0}
|
|
||||||
onCommand={sendCommand}
|
|
||||||
onApproval={sendApproval}
|
|
||||||
onClearNotification={() => clearNotifications(id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<CommandColumn
|
|
||||||
agents={agents}
|
|
||||||
onCommand={sendCommand}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ao-office-section">
|
{selectedAgent && (
|
||||||
<div className="ao-canvas-container" ref={canvasContainerRef} />
|
<SidePanel
|
||||||
</div>
|
agentId={selectedAgent}
|
||||||
|
agentState={agents[selectedAgent]}
|
||||||
{/* 모바일: 에이전트 상세 바텀시트 */}
|
pendingTask={pendingTask}
|
||||||
<MobileSheet
|
onClose={() => setSelectedAgent(null)}
|
||||||
open={!!agentDetailSheet}
|
refreshTrigger={refreshTrigger}
|
||||||
onClose={() => setAgentDetailSheet(null)}
|
|
||||||
title={agentDetailSheet ? (AGENT_META[agentDetailSheet]?.name ?? agentDetailSheet) : ''}
|
|
||||||
>
|
|
||||||
{agentDetailSheet && (
|
|
||||||
<AgentColumn
|
|
||||||
agentId={agentDetailSheet}
|
|
||||||
meta={AGENT_META[agentDetailSheet]}
|
|
||||||
agentState={agents[agentDetailSheet]}
|
|
||||||
notification={notifications[agentDetailSheet] || 0}
|
|
||||||
onCommand={sendCommand}
|
|
||||||
onApproval={sendApproval}
|
|
||||||
onClearNotification={() => clearNotifications(agentDetailSheet)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</MobileSheet>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
return <AgentOffice />;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user