From deb285695ab8119ff28aeb7354a1bea32ec4a58d Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 11 Apr 2026 15:19:14 +0900 Subject: [PATCH] feat(agent-office): notification badges + CEO desk document panel + telegram test - Add notification state management with badge counts in useAgentManager - Render exclamation badge on agent sprites (separate from status icons) - Add CEO desk document icon with click-to-open activity panel - Create DocumentPanel with unified activity feed + per-agent detail tabs - Add telegram test button to stock agent ChatPanel - Remove TaskHistory + bottom toolbar (replaced by DocumentPanel) - Add getActivityFeed API helper Co-Authored-By: Claude Opus 4.6 --- src/api.js | 1 + src/pages/agent-office/AgentOffice.css | 108 +++++++++- src/pages/agent-office/AgentOffice.jsx | 47 +++-- src/pages/agent-office/canvas/AgentSprite.js | 5 + .../agent-office/canvas/OfficeRenderer.js | 84 +++++++- src/pages/agent-office/canvas/SpriteSheet.js | 22 ++ .../agent-office/components/ChatPanel.jsx | 1 + .../agent-office/components/DocumentPanel.jsx | 190 ++++++++++++++++++ .../agent-office/components/TaskHistory.jsx | 62 ------ .../agent-office/hooks/useAgentManager.js | 17 +- .../agent-office/hooks/useOfficeCanvas.js | 18 +- 11 files changed, 459 insertions(+), 96 deletions(-) create mode 100644 src/pages/agent-office/components/DocumentPanel.jsx delete mode 100644 src/pages/agent-office/components/TaskHistory.jsx diff --git a/src/api.js b/src/api.js index c6e3018..04a9d33 100644 --- a/src/api.js +++ b/src/api.js @@ -598,4 +598,5 @@ export const getPendingTasks = () => apiGet('/api/agent-office/tasks export const sendAgentCommand = (agent, action, params={}) => apiPost('/api/agent-office/command', { agent, action, params }); export const approveAgentTask = (agent, task_id, approved, feedback='') => apiPost('/api/agent-office/approve', { agent, task_id, approved, feedback }); export const getAgentStates = () => apiGet('/api/agent-office/states'); +export const getActivityFeed = (limit=50, offset=0) => apiGet(`/api/agent-office/activity?limit=${limit}&offset=${offset}`); diff --git a/src/pages/agent-office/AgentOffice.css b/src/pages/agent-office/AgentOffice.css index fc1b7c9..7d61ca2 100644 --- a/src/pages/agent-office/AgentOffice.css +++ b/src/pages/agent-office/AgentOffice.css @@ -310,22 +310,110 @@ margin: 4px 0 0; } -.ao-toolbar { +/* Document Panel (CEO desk) */ +.ao-doc-panel { + position: absolute; + left: 16px; + top: 60px; + width: 400px; + max-height: calc(100% - 80px); + background: rgba(26, 26, 46, 0.95); + border: 1px solid #333; + border-radius: 12px; + overflow-y: auto; + backdrop-filter: blur(12px); display: flex; - gap: 8px; - padding: 8px 20px; - background: #1a1a2e; - border-top: 1px solid #2a2a4a; + flex-direction: column; } -.ao-tool-btn { - padding: 6px 14px; +.ao-doc-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #2a2a4a; +} + +.ao-doc-title { + font-weight: bold; + font-size: 1rem; + color: #e0e0e0; +} + +.ao-doc-tabs { + display: flex; + gap: 4px; + padding: 8px 16px; + border-bottom: 1px solid #2a2a4a; +} + +.ao-doc-tab { + padding: 4px 12px; border: 1px solid #333; - border-radius: 6px; + border-radius: 8px; background: transparent; - color: #aaa; + color: #888; font-size: 0.8rem; cursor: pointer; font-family: inherit; } -.ao-tool-btn:hover { border-color: #8b5cf6; color: #e0e0e0; } +.ao-doc-tab:hover { color: #ccc; border-color: #555; } +.ao-doc-tab--active { background: rgba(139, 92, 246, 0.2); border-color: #8b5cf6; color: #c4b5fd; } + +.ao-doc-feed { padding: 4px 8px; } +.ao-doc-feed-toolbar { padding: 4px 8px; display: flex; justify-content: flex-end; } + +.ao-doc-feed-item { + padding: 8px 10px; + border-bottom: 1px solid #1a1a2e; +} +.ao-doc-feed-item:last-child { border-bottom: none; } + +.ao-doc-feed-row { + display: flex; + align-items: center; + gap: 6px; +} + +.ao-doc-agent-tag { + font-size: 0.7rem; + padding: 1px 6px; + border-radius: 4px; + color: #fff; +} +.ao-doc-agent-tag--stock { background: #2563eb; } +.ao-doc-agent-tag--music { background: #059669; } + +.ao-doc-feed-msg { + font-size: 0.8rem; + color: #ccc; + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ao-doc-feed-time { + font-size: 0.7rem; + color: #555; + margin-top: 2px; +} + +.ao-doc-tg-status { + font-size: 0.7rem; + margin-left: 4px; +} + +.ao-doc-detail { padding: 0; } +.ao-doc-agent-select { display: flex; gap: 4px; padding: 8px 16px; } +.ao-doc-detail-tabs { display: flex; gap: 4px; padding: 4px 16px; border-bottom: 1px solid #2a2a4a; } + +.ao-doc-log-item { + display: flex; + gap: 6px; + padding: 4px 12px; + font-size: 0.75rem; + border-bottom: 1px solid rgba(255,255,255,0.03); +} +.ao-doc-log-level { font-weight: bold; white-space: nowrap; } +.ao-doc-log-msg { color: #aaa; flex: 1; word-break: break-all; } diff --git a/src/pages/agent-office/AgentOffice.jsx b/src/pages/agent-office/AgentOffice.jsx index 42e4a90..5fadb86 100644 --- a/src/pages/agent-office/AgentOffice.jsx +++ b/src/pages/agent-office/AgentOffice.jsx @@ -2,21 +2,26 @@ 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 DocumentPanel from './components/DocumentPanel'; import './AgentOffice.css'; export function Component() { const canvasContainerRef = useRef(null); const [selectedAgent, setSelectedAgent] = useState(null); - const [showHistory, setShowHistory] = useState(null); + const [showDocument, setShowDocument] = useState(false); - const { agents, pendingTasks, connected, sendCommand, sendApproval } = useAgentManager(); + const { agents, pendingTasks, connected, notifications, sendCommand, sendApproval, clearNotifications } = useAgentManager(); const handleAgentClick = useCallback((agentId) => { setSelectedAgent(prev => prev === agentId ? null : agentId); + clearNotifications(agentId); + }, [clearNotifications]); + + const handleCeoClick = useCallback(() => { + setShowDocument(prev => !prev); }, []); - const { updateAgentState, moveAgent } = useOfficeCanvas(canvasContainerRef, handleAgentClick); + const { updateAgentState, moveAgent, setAgentNotification, setCeoDocBadge } = useOfficeCanvas(canvasContainerRef, handleAgentClick, handleCeoClick); useEffect(() => { for (const [id, info] of Object.entries(agents)) { @@ -24,6 +29,20 @@ export function Component() { } }, [agents, updateAgentState]); + useEffect(() => { + for (const [id, count] of Object.entries(notifications)) { + setAgentNotification(id, count); + } + for (const id of Object.keys(agents)) { + if (!notifications[id]) setAgentNotification(id, 0); + } + }, [notifications, agents, setAgentNotification]); + + useEffect(() => { + const total = Object.values(notifications).reduce((s, n) => s + n, 0); + setCeoDocBadge(total); + }, [notifications, setCeoDocBadge]); + return (
@@ -46,7 +65,9 @@ export function Component() { > {id} - {info.state === 'waiting' && !} + {notifications[id] > 0 && ( + {notifications[id]} + )} ))} {pendingTasks.length > 0 && ( @@ -64,22 +85,10 @@ export function Component() { /> )} - {showHistory && ( - setShowHistory(null)} - /> + {showDocument && ( + setShowDocument(false)} /> )}
- -
- {Object.keys(agents).map(id => ( - - ))} -
); } diff --git a/src/pages/agent-office/canvas/AgentSprite.js b/src/pages/agent-office/canvas/AgentSprite.js index caad3c9..0f0ecfd 100644 --- a/src/pages/agent-office/canvas/AgentSprite.js +++ b/src/pages/agent-office/canvas/AgentSprite.js @@ -6,6 +6,7 @@ export class AgentSprite { this.waypoints = waypoints; this.state = 'idle'; this.detail = ''; + this.notificationCount = 0; const deskKey = `${agentId}_desk`; const desk = waypoints[deskKey] || { x: 5, y: 3 }; @@ -20,6 +21,10 @@ export class AgentSprite { this._moveSpeed = 0.05; } + setNotification(count) { + this.notificationCount = count; + } + setState(newState, detail = '') { this.state = newState; this.detail = detail; diff --git a/src/pages/agent-office/canvas/OfficeRenderer.js b/src/pages/agent-office/canvas/OfficeRenderer.js index 9f37f9b..5feca98 100644 --- a/src/pages/agent-office/canvas/OfficeRenderer.js +++ b/src/pages/agent-office/canvas/OfficeRenderer.js @@ -1,6 +1,6 @@ import { drawTileMap } from './TileMap'; import { AgentSprite } from './AgentSprite'; -import { getCharLabel } from './SpriteSheet'; +import { getCharLabel, drawNotificationBadge } from './SpriteSheet'; const STATUS_ICONS = { idle: null, @@ -19,6 +19,8 @@ export class OfficeRenderer { this.agents = {}; this._animId = null; this._onClick = null; + this._onCeoClick = null; + this._ceoDocBadge = 0; const agentIds = ['stock', 'music']; for (const id of agentIds) { @@ -56,6 +58,21 @@ export class OfficeRenderer { return id; } } + // CEO desk click detection + const ceo = this.mapData.waypoints.ceo_desk; + if (ceo) { + const { scale, offsetX, offsetY, tileSize } = this.renderInfo; + const cx = offsetX + ceo.x * tileSize * scale; + const cy = offsetY + ceo.y * tileSize * scale; + const hitW = 5 * tileSize * scale; + const hitH = 2 * tileSize * scale; + if (canvasX >= cx - tileSize * scale && canvasY >= cy - tileSize * scale && + canvasX <= cx + hitW && canvasY <= cy + hitH) { + if (this._onCeoClick) this._onCeoClick(); + return 'ceo_desk'; + } + } + return null; } @@ -76,6 +93,19 @@ export class OfficeRenderer { } } + setOnCeoClick(handler) { + this._onCeoClick = handler; + } + + setCeoDocBadge(count) { + this._ceoDocBadge = count; + } + + setAgentNotification(agentId, count) { + const sprite = this.agents[agentId]; + if (sprite) sprite.setNotification(count); + } + _loop(timestamp) { const { ctx, canvas, mapData } = this; @@ -95,6 +125,9 @@ export class OfficeRenderer { this._drawOverlay(ctx, sprite, id); } + // CEO desk document icon + this._drawCeoDoc(ctx); + this._animId = requestAnimationFrame(this._loop); } @@ -111,6 +144,11 @@ export class OfficeRenderer { ctx.fillText(icon, cx, cy - 15 * scale); } + // Notification badge (separate from status icon) + if (sprite.notificationCount > 0) { + drawNotificationBadge(ctx, cx, cy - 15 * scale, sprite.notificationCount, scale * 1.5); + } + ctx.fillStyle = 'rgba(255,255,255,0.7)'; ctx.font = `${8 * scale}px monospace`; ctx.textAlign = 'center'; @@ -126,4 +164,48 @@ export class OfficeRenderer { ctx.fillText(sprite.detail, cx, bubbleY); } } + + _drawCeoDoc(ctx) { + if (!this.renderInfo) return; + const ceo = this.mapData.waypoints.ceo_desk; + if (!ceo) return; + + const { scale, offsetX, offsetY, tileSize } = this.renderInfo; + const dx = offsetX + (ceo.x - 1) * tileSize * scale; + const dy = offsetY + (ceo.y - 1) * tileSize * scale; + const docW = 12 * scale; + const docH = 16 * scale; + + // Paper + ctx.fillStyle = '#e8e0d0'; + ctx.fillRect(dx, dy, docW, docH); + // Lines on paper + ctx.fillStyle = '#bbb'; + for (let i = 0; i < 4; i++) { + ctx.fillRect(dx + 2 * scale, dy + (3 + i * 3) * scale, 8 * scale, 1); + } + // Folded corner + ctx.fillStyle = '#d0c8b8'; + ctx.beginPath(); + ctx.moveTo(dx + docW - 3 * scale, dy); + ctx.lineTo(dx + docW, dy + 3 * scale); + ctx.lineTo(dx + docW - 3 * scale, dy + 3 * scale); + ctx.fill(); + + // Badge on document + if (this._ceoDocBadge > 0) { + const bx = dx + docW; + const by = dy; + const r = 4 * scale; + ctx.beginPath(); + ctx.arc(bx, by, r, 0, Math.PI * 2); + ctx.fillStyle = '#f43f5e'; + ctx.fill(); + ctx.fillStyle = '#fff'; + ctx.font = `bold ${5 * scale}px monospace`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this._ceoDocBadge > 9 ? '9+' : String(this._ceoDocBadge), bx, by); + } + } } diff --git a/src/pages/agent-office/canvas/SpriteSheet.js b/src/pages/agent-office/canvas/SpriteSheet.js index 1693617..6460265 100644 --- a/src/pages/agent-office/canvas/SpriteSheet.js +++ b/src/pages/agent-office/canvas/SpriteSheet.js @@ -87,3 +87,25 @@ export function getAnimSpeed(state) { export function getCharLabel(agentId) { return (PIXEL_CHARS[agentId] || {}).label || agentId; } + +export function drawNotificationBadge(ctx, x, y, count, scale = 2) { + const s = scale; + const badgeX = x + 5 * s; + const badgeY = y - 8 * s; + const radius = 5 * s; + + ctx.beginPath(); + ctx.arc(badgeX, badgeY, radius, 0, Math.PI * 2); + ctx.fillStyle = '#f43f5e'; + ctx.fill(); + + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1; + ctx.stroke(); + + ctx.fillStyle = '#fff'; + ctx.font = `bold ${7 * s}px monospace`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('!', badgeX, badgeY); +} diff --git a/src/pages/agent-office/components/ChatPanel.jsx b/src/pages/agent-office/components/ChatPanel.jsx index f9f5363..9aedc87 100644 --- a/src/pages/agent-office/components/ChatPanel.jsx +++ b/src/pages/agent-office/components/ChatPanel.jsx @@ -4,6 +4,7 @@ const AGENT_COMMANDS = { stock: [ { action: 'fetch_news', label: 'λ‰΄μŠ€ μˆ˜μ§‘', icon: 'πŸ“°' }, { action: 'list_alerts', label: 'μ•ŒλžŒ λͺ©λ‘', icon: 'πŸ””' }, + { action: 'test_telegram', label: 'ν…”λ ˆκ·Έλž¨ ν…ŒμŠ€νŠΈ', icon: 'πŸ“¨' }, ], music: [ { action: 'compose', label: 'μž‘κ³‘ μ‹œμž‘', icon: '🎡', needsInput: true }, diff --git a/src/pages/agent-office/components/DocumentPanel.jsx b/src/pages/agent-office/components/DocumentPanel.jsx new file mode 100644 index 0000000..e5033e6 --- /dev/null +++ b/src/pages/agent-office/components/DocumentPanel.jsx @@ -0,0 +1,190 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { getActivityFeed, getAgentTasks, getAgentLogs } from '../../../api'; + +const STATUS_BADGE = { + pending: { label: 'λŒ€κΈ°', color: '#fbbf24' }, + approved: { label: '승인됨', color: '#60a5fa' }, + working: { label: '진행쀑', color: '#818cf8' }, + succeeded: { label: 'μ™„λ£Œ', color: '#34d399' }, + failed: { label: 'μ‹€νŒ¨', color: '#f87171' }, + rejected: { label: '거절됨', color: '#fb923c' }, +}; + +const LOG_LEVEL_COLOR = { + info: '#60a5fa', + warning: '#fbbf24', + error: '#f87171', +}; + +const DocumentPanel = ({ onClose }) => { + const [tab, setTab] = useState('feed'); + const [feed, setFeed] = useState([]); + const [feedLoading, setFeedLoading] = useState(false); + + const [selectedAgent, setSelectedAgent] = useState('stock'); + const [detailTab, setDetailTab] = useState('tasks'); + const [tasks, setTasks] = useState([]); + const [logs, setLogs] = useState([]); + const [detailLoading, setDetailLoading] = useState(false); + + const loadFeed = useCallback(() => { + setFeedLoading(true); + getActivityFeed(80) + .then(data => setFeed(data.items || [])) + .catch(() => setFeed([])) + .finally(() => setFeedLoading(false)); + }, []); + + const loadDetail = useCallback(() => { + setDetailLoading(true); + Promise.all([ + getAgentTasks(selectedAgent, 30).then(d => d.tasks || []).catch(() => []), + getAgentLogs(selectedAgent, 50).then(d => d.logs || []).catch(() => []), + ]).then(([t, l]) => { + setTasks(t); + setLogs(l); + }).finally(() => setDetailLoading(false)); + }, [selectedAgent]); + + useEffect(() => { + if (tab === 'feed') loadFeed(); + else loadDetail(); + }, [tab, loadFeed, loadDetail]); + + const formatTime = (t) => t ? t.replace('T', ' ').slice(0, 19) : ''; + + return ( +
+
+ CEO λ³΄κ³ μ„œ + +
+ +
+ + +
+ + {tab === 'feed' && ( +
+
+ +
+ {feedLoading &&

λ‘œλ”© 쀑...

} + {!feedLoading && feed.length === 0 &&

ν™œλ™ μ—†μŒ

} + {feed.map((item, i) => ( +
+
+ + {item.agent_id} + + {item.type === 'task' ? ( + + {(STATUS_BADGE[item.status] || STATUS_BADGE.pending).label} + + ) : ( + + [{item.level}] + + )} + {item.telegram_sent !== undefined && ( + {item.telegram_sent ? 'TG OK' : 'TG Fail'} + )} +
+
{item.message}
+
+ {formatTime(item.created_at)} + {item.duration_seconds != null && ` Β· ${item.duration_seconds}s`} +
+
+ ))} +
+ )} + + {tab === 'detail' && ( +
+
+ {['stock', 'music'].map(id => ( + + ))} +
+
+ + + +
+ + {detailLoading &&

λ‘œλ”© 쀑...

} + + {!detailLoading && detailTab === 'tasks' && ( +
+ {tasks.length === 0 &&

이λ ₯ μ—†μŒ

} + {tasks.map(task => { + const badge = STATUS_BADGE[task.status] || STATUS_BADGE.pending; + return ( +
+
+ {task.task_type} + + {badge.label} + +
+
+ {formatTime(task.created_at)} + {task.completed_at && ` β†’ ${formatTime(task.completed_at)}`} +
+ {task.result_data && ( +
+ + 결과 보기 + {task.result_data.telegram_sent !== undefined && ( + + {task.result_data.telegram_sent ? ' TG OK' : ' TG Fail'} + + )} + +
{JSON.stringify(task.result_data, null, 2)}
+
+ )} +
+ ); + })} +
+ )} + + {!detailLoading && detailTab === 'logs' && ( +
+ {logs.length === 0 &&

둜그 μ—†μŒ

} + {logs.map(log => ( +
+ + [{log.level}] + + {log.message} + {formatTime(log.created_at)} +
+ ))} +
+ )} +
+ )} +
+ ); +}; + +export default DocumentPanel; diff --git a/src/pages/agent-office/components/TaskHistory.jsx b/src/pages/agent-office/components/TaskHistory.jsx deleted file mode 100644 index 74baf14..0000000 --- a/src/pages/agent-office/components/TaskHistory.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { getAgentTasks } from '../../../api'; - -const STATUS_BADGE = { - pending: { label: 'λŒ€κΈ°', color: '#fbbf24' }, - approved: { label: '승인됨', color: '#60a5fa' }, - working: { label: '진행쀑', color: '#818cf8' }, - succeeded: { label: 'μ™„λ£Œ', color: '#34d399' }, - failed: { label: 'μ‹€νŒ¨', color: '#f87171' }, - rejected: { label: '거절됨', color: '#fb923c' }, -}; - -const TaskHistory = ({ agentId, onClose }) => { - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (!agentId) return; - setLoading(true); - getAgentTasks(agentId, 30) - .then(data => setTasks(data.tasks || [])) - .catch(() => setTasks([])) - .finally(() => setLoading(false)); - }, [agentId]); - - return ( -
-
- μž‘μ—… 이λ ₯ β€” {agentId} - -
-
- {loading &&

λ‘œλ”© 쀑...

} - {!loading && tasks.length === 0 &&

이λ ₯ μ—†μŒ

} - {tasks.map(task => { - const badge = STATUS_BADGE[task.status] || STATUS_BADGE.pending; - return ( -
-
- {task.task_type} - - {badge.label} - -
-
- {task.created_at?.replace('T', ' ').slice(0, 19)} -
- {task.result_data && ( -
- 결과 보기 -
{JSON.stringify(task.result_data, null, 2)}
-
- )} -
- ); - })} -
-
- ); -}; - -export default TaskHistory; diff --git a/src/pages/agent-office/hooks/useAgentManager.js b/src/pages/agent-office/hooks/useAgentManager.js index b7c8d5e..1291a07 100644 --- a/src/pages/agent-office/hooks/useAgentManager.js +++ b/src/pages/agent-office/hooks/useAgentManager.js @@ -4,6 +4,7 @@ export function useAgentManager() { const [agents, setAgents] = useState({}); const [pendingTasks, setPendingTasks] = useState([]); const [connected, setConnected] = useState(false); + const [notifications, setNotifications] = useState({}); const wsRef = useRef(null); const reconnectTimer = useRef(null); @@ -58,6 +59,12 @@ export function useAgentManager() { [msg.agent]: { ...prev[msg.agent], lastCommand: msg.result }, })); break; + case 'notification': + setNotifications(prev => ({ + ...prev, + [msg.agent]: (prev[msg.agent] || 0) + 1, + })); + break; default: break; } @@ -84,5 +91,13 @@ export function useAgentManager() { } }, []); - return { agents, pendingTasks, connected, sendCommand, sendApproval }; + const clearNotifications = useCallback((agentId) => { + setNotifications(prev => { + const next = { ...prev }; + delete next[agentId]; + return next; + }); + }, []); + + return { agents, pendingTasks, connected, notifications, sendCommand, sendApproval, clearNotifications }; } diff --git a/src/pages/agent-office/hooks/useOfficeCanvas.js b/src/pages/agent-office/hooks/useOfficeCanvas.js index f276be9..f6905c3 100644 --- a/src/pages/agent-office/hooks/useOfficeCanvas.js +++ b/src/pages/agent-office/hooks/useOfficeCanvas.js @@ -2,7 +2,7 @@ import { useRef, useEffect, useCallback } from 'react'; import { OfficeRenderer } from '../canvas/OfficeRenderer'; import officeMap from '../assets/office-map.json'; -export function useOfficeCanvas(containerRef, onAgentClick) { +export function useOfficeCanvas(containerRef, onAgentClick, onCeoClick) { const rendererRef = useRef(null); useEffect(() => { @@ -30,6 +30,10 @@ export function useOfficeCanvas(containerRef, onAgentClick) { if (onAgentClick) onAgentClick(agentId); }); + renderer.setOnCeoClick(() => { + if (onCeoClick) onCeoClick(); + }); + const handleClick = (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; @@ -48,7 +52,7 @@ export function useOfficeCanvas(containerRef, onAgentClick) { containerRef.current.removeChild(canvas); } }; - }, [containerRef, onAgentClick]); + }, [containerRef, onAgentClick, onCeoClick]); const updateAgentState = useCallback((agentId, state, detail) => { rendererRef.current?.updateAgentState(agentId, state, detail); @@ -58,5 +62,13 @@ export function useOfficeCanvas(containerRef, onAgentClick) { rendererRef.current?.moveAgent(agentId, target); }, []); - return { updateAgentState, moveAgent }; + const setAgentNotification = useCallback((agentId, count) => { + rendererRef.current?.setAgentNotification(agentId, count); + }, []); + + const setCeoDocBadge = useCallback((count) => { + rendererRef.current?.setCeoDocBadge(count); + }, []); + + return { updateAgentState, moveAgent, setAgentNotification, setCeoDocBadge }; }