feat(agent-office): 모바일 반응형 세로 스택 + 작업 시간 표기 개선
- 768px 이하에서 대시보드 세로 스택 + 에이전트 카드 아코디언 토글 - waiting/알림 있을 때 자동 펼침 및 좌측 강조 바 - 픽셀 오피스 캔버스 모바일 높이 140px로 축소 후 상단 배치 - 최근 작업 시간: completed_at 우선 + 오늘/어제/MM-DD HH:MM 포맷 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,21 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ao-col-chevron {
|
||||||
|
display: none;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.ao-col-name {
|
.ao-col-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -303,3 +318,71 @@
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile: vertical stack + accordion */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ao-page {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-dashboard {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
overflow: visible;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col {
|
||||||
|
flex: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col-header {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col-chevron {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col--collapsed .ao-col-body {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col--attention {
|
||||||
|
box-shadow: inset 3px 0 0 #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col-tasks {
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-office-section {
|
||||||
|
height: 140px;
|
||||||
|
order: -1;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: 2px solid #2a2a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-col-commands {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ao-cmd-btn,
|
||||||
|
.ao-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,9 +35,12 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
|
|||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [activeCommand, setActiveCommand] = useState(null);
|
const [activeCommand, setActiveCommand] = useState(null);
|
||||||
const [tokenUsage, setTokenUsage] = useState(null);
|
const [tokenUsage, setTokenUsage] = useState(null);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const state = agentState || { state: 'offline' };
|
const state = agentState || { state: 'offline' };
|
||||||
const commands = AGENT_COMMANDS[agentId] || [];
|
const commands = AGENT_COMMANDS[agentId] || [];
|
||||||
|
const needsAttention = state.state === 'waiting' || notification > 0;
|
||||||
|
const isOpen = expanded || needsAttention;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAgentTasks(agentId, 10)
|
getAgentTasks(agentId, 10)
|
||||||
@@ -89,11 +92,31 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
|
|||||||
setActiveCommand(null);
|
setActiveCommand(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (t) => t ? t.replace('T', ' ').slice(11, 19) : '';
|
const formatTaskTime = (task) => {
|
||||||
|
const iso = task.completed_at || task.created_at;
|
||||||
|
if (!iso) return '';
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (isNaN(d.getTime())) return '';
|
||||||
|
const now = new Date();
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
const sameDay = d.toDateString() === now.toDateString();
|
||||||
|
const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1);
|
||||||
|
const isYesterday = d.toDateString() === yesterday.toDateString();
|
||||||
|
if (sameDay) return `오늘 ${hm}`;
|
||||||
|
if (isYesterday) return `어제 ${hm}`;
|
||||||
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${hm}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeaderClick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setExpanded(v => !v);
|
||||||
|
onClearNotification();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ao-col" onClick={onClearNotification}>
|
<div className={`ao-col ${isOpen ? 'ao-col--open' : 'ao-col--collapsed'} ${needsAttention ? 'ao-col--attention' : ''}`} onClick={onClearNotification}>
|
||||||
<div className="ao-col-header" style={{ borderColor: meta.color }}>
|
<div className="ao-col-header" style={{ borderColor: meta.color }} onClick={handleHeaderClick}>
|
||||||
<span className="ao-col-name" style={{ color: meta.color }}>{meta.name}</span>
|
<span className="ao-col-name" style={{ color: meta.color }}>{meta.name}</span>
|
||||||
{tokenUsage && tokenUsage.total_tokens > 0 && (
|
{tokenUsage && tokenUsage.total_tokens > 0 && (
|
||||||
<span
|
<span
|
||||||
@@ -105,8 +128,12 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
|
|||||||
)}
|
)}
|
||||||
<span className={`ao-col-state ao-col-state--${state.state}`}>{state.state}</span>
|
<span className={`ao-col-state ao-col-state--${state.state}`}>{state.state}</span>
|
||||||
{notification > 0 && <span className="ao-col-badge">{notification}</span>}
|
{notification > 0 && <span className="ao-col-badge">{notification}</span>}
|
||||||
|
<span className="ao-col-chevron" aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ao-col-body">
|
||||||
|
|
||||||
|
|
||||||
{state.detail && (
|
{state.detail && (
|
||||||
<div className="ao-col-detail">{state.detail}</div>
|
<div className="ao-col-detail">{state.detail}</div>
|
||||||
)}
|
)}
|
||||||
@@ -153,7 +180,7 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
|
|||||||
<span className="ao-col-task-badge" style={{ background: badge.bg }}>{badge.label}</span>
|
<span className="ao-col-task-badge" style={{ background: badge.bg }}>{badge.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ao-col-task-time">
|
<div className="ao-col-task-time">
|
||||||
{formatTime(task.created_at)}
|
{formatTaskTime(task)}
|
||||||
{task.result_data?.telegram_sent !== undefined && (
|
{task.result_data?.telegram_sent !== undefined && (
|
||||||
<span className="ao-doc-tg-status">{task.result_data.telegram_sent ? ' TG OK' : ' TG Fail'}</span>
|
<span className="ao-doc-tg-status">{task.result_data.telegram_sent ? ' TG OK' : ' TG Fail'}</span>
|
||||||
)}
|
)}
|
||||||
@@ -168,6 +195,7 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user