feat(agent-office): Blog + Realestate 에이전트 UI 추가
- AGENT_META/IDS에 blog/realestate 추가 (4 컬럼 대시보드) - SpriteSheet: 블로그(노트북 액센트)/청약(서류가방 액센트) 픽셀 캐릭터 - office-map: 사무실 책상 4개로 확장, blog_desk/realestate_desk waypoint 추가 - AgentColumn/ChatPanel: 에이전트별 퀵 명령 버튼 (키워드 리서치, 매칭 리포트 등) - CommandColumn: 타겟 선택지 4명, 빠른 명령 6개, 아이콘 맵핑 - DocumentPanel: 에이전트별 탭 4개 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,12 @@ import './AgentOffice.css';
|
||||
const AGENT_META = {
|
||||
stock: { name: '주식 트레이더', color: '#4488cc' },
|
||||
music: { name: '음악 프로듀서', color: '#44aa88' },
|
||||
blog: { name: '블로그 마케터', color: '#d97706' },
|
||||
realestate: { name: '청약 애널리스트', color: '#c026d3' },
|
||||
};
|
||||
|
||||
const AGENT_IDS = ['stock', 'music', 'blog', 'realestate'];
|
||||
|
||||
export function Component() {
|
||||
const canvasContainerRef = useRef(null);
|
||||
|
||||
@@ -54,7 +58,7 @@ export function Component() {
|
||||
</div>
|
||||
|
||||
<div className="ao-dashboard">
|
||||
{['stock', 'music'].map(id => (
|
||||
{AGENT_IDS.map(id => (
|
||||
<AgentColumn
|
||||
key={id}
|
||||
agentId={id}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"furniture": [
|
||||
{"type": "desk", "x": 2, "y": 1, "label": "Stock"},
|
||||
{"type": "desk", "x": 7, "y": 1, "label": "Music"},
|
||||
{"type": "desk", "x": 12, "y": 1, "label": "Claude"},
|
||||
{"type": "desk", "x": 17, "y": 1, "label": "(빈)"},
|
||||
{"type": "desk", "x": 12, "y": 1, "label": "Blog"},
|
||||
{"type": "desk", "x": 17, "y": 1, "label": "Realestate"},
|
||||
{"type": "table", "x": 8, "y": 6, "w": 4, "h": 2, "label": "회의 테이블"},
|
||||
{"type": "sofa", "x": 1, "y": 10, "label": "휴게실"},
|
||||
{"type": "coffee", "x": 3, "y": 10, "label": "☕"},
|
||||
@@ -33,7 +33,8 @@
|
||||
"waypoints": {
|
||||
"stock_desk": {"x": 2, "y": 2},
|
||||
"music_desk": {"x": 7, "y": 2},
|
||||
"claude_desk": {"x": 12, "y": 2},
|
||||
"blog_desk": {"x": 12, "y": 2},
|
||||
"realestate_desk": {"x": 17, "y": 2},
|
||||
"meeting_table": {"x": 9, "y": 7},
|
||||
"break_room": {"x": 2, "y": 11},
|
||||
"ceo_desk": {"x": 16, "y": 11}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class OfficeRenderer {
|
||||
this._onCeoClick = null;
|
||||
this._ceoDocBadge = 0;
|
||||
|
||||
const agentIds = ['stock', 'music'];
|
||||
const agentIds = ['stock', 'music', 'blog', 'realestate'];
|
||||
for (const id of agentIds) {
|
||||
this.agents[id] = new AgentSprite(id, mapData.waypoints);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const PIXEL_CHARS = {
|
||||
stock: { body: '#4488cc', accent: '#cc4444', label: '주식', hair: '#332222' },
|
||||
music: { body: '#44aa88', accent: '#ffaa00', label: '음악', hair: '#443322' },
|
||||
blog: { body: '#d97706', accent: '#fde68a', label: '블로그', hair: '#3b2a1a' },
|
||||
realestate: { body: '#c026d3', accent: '#86efac', label: '청약', hair: '#2a2a3a' },
|
||||
claude: { body: '#8855cc', accent: '#cc88ff', label: 'Claude', hair: '#554466' },
|
||||
};
|
||||
|
||||
@@ -57,6 +59,14 @@ export function drawAgent(ctx, agentId, x, y, state, frameIndex, scale = 2) {
|
||||
ctx.fillRect(-4 * s, -4 * s, 1 * s, 4 * s);
|
||||
ctx.fillRect(3 * s, -4 * s, 1 * s, 4 * s);
|
||||
ctx.fillRect(-4 * s, -5 * s, 8 * s, 1 * s);
|
||||
} else if (agentId === 'blog') {
|
||||
// 노트북 액센트 (무릎 위)
|
||||
ctx.fillRect(-3 * s, 6 * s, 6 * s, 1 * s);
|
||||
ctx.fillRect(-3 * s, 7 * s, 6 * s, 2 * s);
|
||||
} else if (agentId === 'realestate') {
|
||||
// 서류 가방 액센트 (손 옆)
|
||||
ctx.fillRect(3 * s, 4 * s, 2 * s, 3 * s);
|
||||
ctx.fillRect(3 * s, 3 * s, 2 * s, 1 * s);
|
||||
} else if (agentId === 'claude') {
|
||||
ctx.globalAlpha = 0.3 + 0.2 * Math.sin(Date.now() / 500);
|
||||
ctx.fillRect(-4 * s, -6 * s, 8 * s, 1 * s);
|
||||
|
||||
@@ -20,6 +20,14 @@ const AGENT_COMMANDS = {
|
||||
{ action: 'compose', label: '작곡 시작', icon: '🎵', needsInput: true },
|
||||
{ action: 'credits', label: '크레딧', icon: '💳' },
|
||||
],
|
||||
blog: [
|
||||
{ action: 'research', label: '키워드 리서치', icon: '🔍', needsInput: true },
|
||||
{ action: 'list_trend_keywords', label: '트렌드 목록', icon: '📋' },
|
||||
],
|
||||
realestate: [
|
||||
{ action: 'fetch_matches', label: '매칭 리포트', icon: '🏢' },
|
||||
{ action: 'dashboard', label: '대시보드', icon: '📊' },
|
||||
],
|
||||
};
|
||||
|
||||
const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApproval, onClearNotification }) => {
|
||||
@@ -73,7 +81,10 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || !activeCommand) return;
|
||||
onCommand(agentId, activeCommand, activeCommand === 'compose' ? { prompt: input } : { message: input });
|
||||
const params = activeCommand === 'compose' ? { prompt: input }
|
||||
: activeCommand === 'research' ? { keyword: input }
|
||||
: { message: input };
|
||||
onCommand(agentId, activeCommand, params);
|
||||
setInput('');
|
||||
setActiveCommand(null);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,21 @@ const AGENT_COMMANDS = {
|
||||
{ action: 'compose', label: '작곡 시작', icon: '🎵', needsInput: true },
|
||||
{ action: 'credits', label: '크레딧 확인', icon: '💳' },
|
||||
],
|
||||
blog: [
|
||||
{ action: 'research', label: '키워드 리서치', icon: '🔍', needsInput: true },
|
||||
{ action: 'list_trend_keywords', label: '트렌드 목록', icon: '📋' },
|
||||
],
|
||||
realestate: [
|
||||
{ action: 'fetch_matches', label: '매칭 리포트', icon: '🏢' },
|
||||
{ action: 'dashboard', label: '대시보드', icon: '📊' },
|
||||
],
|
||||
};
|
||||
|
||||
const AGENT_NAMES = {
|
||||
stock: '주식 트레이더',
|
||||
music: '음악 프로듀서',
|
||||
blog: '블로그 마케터',
|
||||
realestate: '청약 애널리스트',
|
||||
};
|
||||
|
||||
const ChatPanel = ({ agentId, agentState, onCommand, onApproval, onClose }) => {
|
||||
@@ -21,8 +36,8 @@ const ChatPanel = ({ agentId, agentState, onCommand, onApproval, onClose }) => {
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || !activeCommand) return;
|
||||
const params = activeCommand === 'compose'
|
||||
? { prompt: input }
|
||||
const params = activeCommand === 'compose' ? { prompt: input }
|
||||
: activeCommand === 'research' ? { keyword: input }
|
||||
: { message: input };
|
||||
onCommand(agentId, activeCommand, params);
|
||||
setInput('');
|
||||
@@ -41,8 +56,7 @@ const ChatPanel = ({ agentId, agentState, onCommand, onApproval, onClose }) => {
|
||||
<div className="ao-chat-panel">
|
||||
<div className="ao-chat-header">
|
||||
<span className="ao-chat-title">
|
||||
{agentId === 'stock' ? '주식 트레이더' :
|
||||
agentId === 'music' ? '음악 프로듀서' : agentId}
|
||||
{AGENT_NAMES[agentId] || agentId}
|
||||
</span>
|
||||
<span className={`ao-chat-state ao-chat-state--${state.state || 'idle'}`}>
|
||||
{state.state || 'idle'}
|
||||
@@ -87,7 +101,11 @@ const ChatPanel = ({ agentId, agentState, onCommand, onApproval, onClose }) => {
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSend()}
|
||||
placeholder={activeCommand === 'compose' ? '프롬프트 입력...' : '메시지 입력...'}
|
||||
placeholder={
|
||||
activeCommand === 'compose' ? '프롬프트 입력...'
|
||||
: activeCommand === 'research' ? '키워드 입력...'
|
||||
: '메시지 입력...'
|
||||
}
|
||||
autoFocus
|
||||
/>
|
||||
<button className="ao-btn ao-btn--send" onClick={handleSend}>전송</button>
|
||||
|
||||
@@ -3,12 +3,24 @@ import React, { useState } from 'react';
|
||||
const TARGETS = [
|
||||
{ id: 'stock', name: '주식 트레이더' },
|
||||
{ id: 'music', name: '음악 프로듀서' },
|
||||
{ id: 'blog', name: '블로그 마케터' },
|
||||
{ id: 'realestate', name: '청약 애널리스트' },
|
||||
];
|
||||
|
||||
const TARGET_ICONS = {
|
||||
stock: '📈',
|
||||
music: '🎵',
|
||||
blog: '✍️',
|
||||
realestate: '🏢',
|
||||
};
|
||||
|
||||
const QUICK_COMMANDS = [
|
||||
{ target: 'stock', action: 'fetch_news', label: '뉴스 수집' },
|
||||
{ target: 'stock', action: 'test_telegram', label: 'TG 테스트' },
|
||||
{ target: 'music', action: 'credits', label: '크레딧 확인' },
|
||||
{ target: 'blog', action: 'list_trend_keywords', label: '트렌드 목록' },
|
||||
{ target: 'realestate', action: 'fetch_matches', label: '매칭 리포트' },
|
||||
{ target: 'realestate', action: 'dashboard', label: '청약 대시보드' },
|
||||
];
|
||||
|
||||
const CommandColumn = ({ agents, onCommand }) => {
|
||||
@@ -79,7 +91,7 @@ const CommandColumn = ({ agents, onCommand }) => {
|
||||
<div className="ao-col-commands">
|
||||
{QUICK_COMMANDS.map((cmd, i) => (
|
||||
<button key={i} className="ao-cmd-btn" onClick={() => handleQuick(cmd)}>
|
||||
{cmd.target === 'stock' ? '📈' : '🎵'} {cmd.label}
|
||||
{TARGET_ICONS[cmd.target] || '🤖'} {cmd.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -110,11 +110,16 @@ const DocumentPanel = ({ onClose }) => {
|
||||
{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>
|
||||
{[
|
||||
{ id: 'stock', name: '주식 트레이더' },
|
||||
{ id: 'music', name: '음악 프로듀서' },
|
||||
{ id: 'blog', name: '블로그 마케터' },
|
||||
{ id: 'realestate', name: '청약 애널리스트' },
|
||||
].map(a => (
|
||||
<button key={a.id}
|
||||
className={`ao-doc-tab ${selectedAgent === a.id ? 'ao-doc-tab--active' : ''}`}
|
||||
onClick={() => setSelectedAgent(a.id)}
|
||||
>{a.name}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="ao-doc-detail-tabs">
|
||||
|
||||
Reference in New Issue
Block a user