Compare commits

4 Commits

Author SHA1 Message Date
0dce449124 chore(agent-office): convert agent PNGs to WebP (~93% smaller)
ffmpeg libwebp quality=85 compression_level=6.
Total: 11.8MB → 875KB (~11MB saved). Visually indistinguishable on
the card grid at the 9:16 image aspect.

PNG removals were already staged in the previous CommandTab commit;
this commit adds the 6 .webp replacements and points constants.js
imports at .webp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:58:12 +09:00
2c32659f6a fix(agent-office): useAgentManager reconnect via ref to satisfy lint
Previously connect's onclose handler referenced connect itself before
the useCallback declaration, triggering react-hooks/immutability. Hold
the latest connect in a ref (updated in useEffect) and call through it
on reconnect. Same runtime behavior, lint-clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:58:04 +09:00
add2d8044c style(agent-office): neutral color for sidepanel state line
Was hardcoded #22c55e (green) regardless of actual state, making
error/break states look healthy. Switch to muted #94a3b8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:57:57 +09:00
2e9b0daec6 fix(agent-office): CommandTab approval state + blog→insta agent
- Approval card gated on 'waiting_approval' (was 'waiting'), matching
  the state useAgentManager emits — previously the approval UI was
  silently suppressed and pendingTask buttons unreachable
- QUICK_ACTIONS/PARAM_ACTIONS: drop blog (agent removed),
  add insta (extract / collect_trends / render)
- Regression test covers the three approval-card branches

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:57:41 +09:00
17 changed files with 69 additions and 11 deletions

View File

@@ -194,7 +194,7 @@
}
.ao-sidepanel-state {
font-size: 11px;
color: #22c55e;
color: #94a3b8;
}
.ao-sidepanel-close {
background: none;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -5,7 +5,7 @@ import { sendAgentCommand, approveAgentTask } from '../../../api';
const QUICK_ACTIONS = {
stock: [{ action: 'fetch_news', label: 'Fetch News' }, { action: 'test_telegram', label: 'Test Telegram' }],
music: [{ action: 'credits', label: 'Check Credits' }],
blog: [{ action: 'list_trend_keywords', label: 'List Keywords' }],
insta: [{ action: 'extract', label: 'Extract News' }, { action: 'collect_trends', label: 'Collect Trends' }],
realestate: [{ action: 'dashboard', label: 'Dashboard' }],
lotto: [{ action: 'status', label: 'Status' }, { action: 'curate_now', label: 'Curate Now' }]
};
@@ -13,7 +13,7 @@ const QUICK_ACTIONS = {
const PARAM_ACTIONS = {
stock: { action: 'add_alert', label: 'Add Alert', placeholder: '{"symbol":"005930","target_price":70000,"direction":"above"}' },
music: { action: 'compose', label: 'Compose', placeholder: 'jazzy lo-fi piano beat' },
blog: { action: 'research', label: 'Research', placeholder: 'keyword to research' },
insta: { action: 'render', label: 'Render Slate', placeholder: 'keyword_id (예: 42)' },
realestate: { action: 'fetch_matches', label: 'Fetch Matches', placeholder: '' },
lotto: null
};
@@ -46,6 +46,8 @@ export default function CommandTab({ agentId, agentState, pendingTask, onCommand
params = { prompt: paramInput };
} else if (paramAction.action === 'research') {
params = { keyword: paramInput };
} else if (paramAction.action === 'render') {
params = { keyword_id: parseInt(paramInput, 10) };
} else {
try { params = JSON.parse(paramInput); } catch { params = { value: paramInput }; }
}
@@ -87,7 +89,7 @@ export default function CommandTab({ agentId, agentState, pendingTask, onCommand
return (
<div className="ao-command-tab">
{/* 승인 대기 UI */}
{agentState === 'waiting' && pendingTask && (
{agentState === 'waiting_approval' && pendingTask && (
<div className="ao-approval-card">
<div className="ao-approval-title">Awaiting Approval</div>
<div className="ao-approval-desc">{pendingTask.task_type}: {pendingTask.detail || JSON.stringify(pendingTask.input_data)}</div>

View File

@@ -0,0 +1,51 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import CommandTab from './CommandTab.jsx';
vi.mock('../../../api', () => ({
sendAgentCommand: vi.fn(),
approveAgentTask: vi.fn(),
}));
describe('CommandTab approval card', () => {
const samplePendingTask = {
id: 'task-123',
task_type: 'lotto_briefing',
input_data: { draw_no: 1234 },
};
it('agentState가 waiting_approval이고 pendingTask가 있으면 승인 카드를 표시', () => {
render(
<CommandTab
agentId="lotto"
agentState="waiting_approval"
pendingTask={samplePendingTask}
/>
);
expect(screen.getByText('Awaiting Approval')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Approve' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Reject' })).toBeInTheDocument();
});
it('agentState가 working이면 승인 카드를 표시하지 않음', () => {
render(
<CommandTab
agentId="lotto"
agentState="working"
pendingTask={samplePendingTask}
/>
);
expect(screen.queryByText('Awaiting Approval')).toBeNull();
});
it('pendingTask가 null이면 waiting_approval이어도 승인 카드를 표시하지 않음', () => {
render(
<CommandTab
agentId="lotto"
agentState="waiting_approval"
pendingTask={null}
/>
);
expect(screen.queryByText('Awaiting Approval')).toBeNull();
});
});

View File

@@ -1,10 +1,10 @@
// src/pages/agent-office/constants.js
import stockImg from './assets/agent_stock.png';
import musicImg from './assets/agent_music.png';
import instaImg from './assets/agent_insta.png';
import realestateImg from './assets/agent_realestate.png';
import lottoImg from './assets/agent_lotto.png';
import undeterminedImg from './assets/agent_undetermined.png';
import stockImg from './assets/agent_stock.webp';
import musicImg from './assets/agent_music.webp';
import instaImg from './assets/agent_insta.webp';
import realestateImg from './assets/agent_realestate.webp';
import lottoImg from './assets/agent_lotto.webp';
import undeterminedImg from './assets/agent_undetermined.webp';
export const AGENT_META = {
stock: { displayName: '주식 트레이더', color: '#4488cc', image: stockImg },

View File

@@ -12,6 +12,7 @@ export function useAgentManager() {
const wsRef = useRef(null);
const reconnectRef = useRef(null);
const connectRef = useRef(null);
const connect = useCallback(() => {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
@@ -68,12 +69,16 @@ export function useAgentManager() {
ws.onclose = () => {
setConnected(false);
reconnectRef.current = setTimeout(connect, WS_RECONNECT_DELAY);
reconnectRef.current = setTimeout(() => connectRef.current?.(), WS_RECONNECT_DELAY);
};
ws.onerror = () => ws.close();
}, []);
useEffect(() => {
connectRef.current = connect;
}, [connect]);
useEffect(() => {
connect();
return () => {