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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 },
|
||||
|
||||
190
src/pages/agent-office/components/DocumentPanel.jsx
Normal file
190
src/pages/agent-office/components/DocumentPanel.jsx
Normal file
@@ -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 (
|
||||
<div className="ao-doc-panel">
|
||||
<div className="ao-doc-header">
|
||||
<span className="ao-doc-title">CEO 보고서</span>
|
||||
<button className="ao-chat-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="ao-doc-tabs">
|
||||
<button
|
||||
className={`ao-doc-tab ${tab === 'feed' ? 'ao-doc-tab--active' : ''}`}
|
||||
onClick={() => setTab('feed')}
|
||||
>활동 피드</button>
|
||||
<button
|
||||
className={`ao-doc-tab ${tab === 'detail' ? 'ao-doc-tab--active' : ''}`}
|
||||
onClick={() => setTab('detail')}
|
||||
>에이전트별</button>
|
||||
</div>
|
||||
|
||||
{tab === 'feed' && (
|
||||
<div className="ao-doc-feed">
|
||||
<div className="ao-doc-feed-toolbar">
|
||||
<button className="ao-cmd-btn" onClick={loadFeed}>새로고침</button>
|
||||
</div>
|
||||
{feedLoading && <p className="ao-history-empty">로딩 중...</p>}
|
||||
{!feedLoading && feed.length === 0 && <p className="ao-history-empty">활동 없음</p>}
|
||||
{feed.map((item, i) => (
|
||||
<div key={i} className="ao-doc-feed-item">
|
||||
<div className="ao-doc-feed-row">
|
||||
<span className={`ao-doc-agent-tag ao-doc-agent-tag--${item.agent_id}`}>
|
||||
{item.agent_id}
|
||||
</span>
|
||||
{item.type === 'task' ? (
|
||||
<span className="ao-history-badge" style={{ background: (STATUS_BADGE[item.status] || STATUS_BADGE.pending).color }}>
|
||||
{(STATUS_BADGE[item.status] || STATUS_BADGE.pending).label}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ao-doc-log-level" style={{ color: LOG_LEVEL_COLOR[item.level] || '#888' }}>
|
||||
[{item.level}]
|
||||
</span>
|
||||
)}
|
||||
{item.telegram_sent !== undefined && (
|
||||
<span className="ao-doc-tg-status">{item.telegram_sent ? 'TG OK' : 'TG Fail'}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="ao-doc-feed-msg">{item.message}</div>
|
||||
<div className="ao-doc-feed-time">
|
||||
{formatTime(item.created_at)}
|
||||
{item.duration_seconds != null && ` · ${item.duration_seconds}s`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'detail' && (
|
||||
<div className="ao-doc-detail">
|
||||
<div className="ao-doc-agent-select">
|
||||
{['stock', 'music'].map(id => (
|
||||
<button key={id}
|
||||
className={`ao-doc-tab ${selectedAgent === id ? 'ao-doc-tab--active' : ''}`}
|
||||
onClick={() => setSelectedAgent(id)}
|
||||
>{id === 'stock' ? '주식 트레이더' : '음악 프로듀서'}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="ao-doc-detail-tabs">
|
||||
<button
|
||||
className={`ao-doc-tab ${detailTab === 'tasks' ? 'ao-doc-tab--active' : ''}`}
|
||||
onClick={() => setDetailTab('tasks')}
|
||||
>작업 ({tasks.length})</button>
|
||||
<button
|
||||
className={`ao-doc-tab ${detailTab === 'logs' ? 'ao-doc-tab--active' : ''}`}
|
||||
onClick={() => setDetailTab('logs')}
|
||||
>로그 ({logs.length})</button>
|
||||
<button className="ao-cmd-btn" onClick={loadDetail} style={{marginLeft:'auto'}}>새로고침</button>
|
||||
</div>
|
||||
|
||||
{detailLoading && <p className="ao-history-empty">로딩 중...</p>}
|
||||
|
||||
{!detailLoading && detailTab === 'tasks' && (
|
||||
<div className="ao-history-list">
|
||||
{tasks.length === 0 && <p className="ao-history-empty">이력 없음</p>}
|
||||
{tasks.map(task => {
|
||||
const badge = STATUS_BADGE[task.status] || STATUS_BADGE.pending;
|
||||
return (
|
||||
<div key={task.id} className="ao-history-item">
|
||||
<div className="ao-history-item-header">
|
||||
<span className="ao-history-type">{task.task_type}</span>
|
||||
<span className="ao-history-badge" style={{ background: badge.color }}>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ao-history-time">
|
||||
{formatTime(task.created_at)}
|
||||
{task.completed_at && ` → ${formatTime(task.completed_at)}`}
|
||||
</div>
|
||||
{task.result_data && (
|
||||
<details className="ao-history-detail">
|
||||
<summary>
|
||||
결과 보기
|
||||
{task.result_data.telegram_sent !== undefined && (
|
||||
<span className="ao-doc-tg-status">
|
||||
{task.result_data.telegram_sent ? ' TG OK' : ' TG Fail'}
|
||||
</span>
|
||||
)}
|
||||
</summary>
|
||||
<pre>{JSON.stringify(task.result_data, null, 2)}</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!detailLoading && detailTab === 'logs' && (
|
||||
<div className="ao-history-list">
|
||||
{logs.length === 0 && <p className="ao-history-empty">로그 없음</p>}
|
||||
{logs.map(log => (
|
||||
<div key={log.id} className="ao-doc-log-item">
|
||||
<span className="ao-doc-log-level" style={{ color: LOG_LEVEL_COLOR[log.level] || '#888' }}>
|
||||
[{log.level}]
|
||||
</span>
|
||||
<span className="ao-doc-log-msg">{log.message}</span>
|
||||
<span className="ao-doc-feed-time">{formatTime(log.created_at)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentPanel;
|
||||
@@ -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 (
|
||||
<div className="ao-history-panel">
|
||||
<div className="ao-history-header">
|
||||
<span>작업 이력 — {agentId}</span>
|
||||
<button className="ao-chat-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
<div className="ao-history-list">
|
||||
{loading && <p className="ao-history-empty">로딩 중...</p>}
|
||||
{!loading && tasks.length === 0 && <p className="ao-history-empty">이력 없음</p>}
|
||||
{tasks.map(task => {
|
||||
const badge = STATUS_BADGE[task.status] || STATUS_BADGE.pending;
|
||||
return (
|
||||
<div key={task.id} className="ao-history-item">
|
||||
<div className="ao-history-item-header">
|
||||
<span className="ao-history-type">{task.task_type}</span>
|
||||
<span className="ao-history-badge" style={{ background: badge.color }}>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ao-history-time">
|
||||
{task.created_at?.replace('T', ' ').slice(0, 19)}
|
||||
</div>
|
||||
{task.result_data && (
|
||||
<details className="ao-history-detail">
|
||||
<summary>결과 보기</summary>
|
||||
<pre>{JSON.stringify(task.result_data, null, 2)}</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskHistory;
|
||||
Reference in New Issue
Block a user