feat(agent-office): add CommandTab with quick actions, params, and approval UI
This commit is contained in:
164
src/pages/agent-office/components/CommandTab.jsx
Normal file
164
src/pages/agent-office/components/CommandTab.jsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
// src/pages/agent-office/components/CommandTab.jsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
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' }],
|
||||||
|
realestate: [{ action: 'dashboard', label: 'Dashboard' }],
|
||||||
|
lotto: [{ action: 'status', label: 'Status' }, { action: 'curate_now', label: 'Curate Now' }]
|
||||||
|
};
|
||||||
|
|
||||||
|
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' },
|
||||||
|
realestate: { action: 'fetch_matches', label: 'Fetch Matches', placeholder: '' },
|
||||||
|
lotto: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CommandTab({ agentId, agentState, pendingTask, onCommandResult }) {
|
||||||
|
const [customAction, setCustomAction] = useState('');
|
||||||
|
const [customParams, setCustomParams] = useState('');
|
||||||
|
const [paramInput, setParamInput] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const quickActions = QUICK_ACTIONS[agentId] || [];
|
||||||
|
const paramAction = PARAM_ACTIONS[agentId];
|
||||||
|
|
||||||
|
const handleQuickAction = async (action) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await sendAgentCommand(agentId, action, {});
|
||||||
|
onCommandResult?.(result);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParamAction = async () => {
|
||||||
|
if (!paramAction || !paramInput.trim()) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
let params = {};
|
||||||
|
if (paramAction.action === 'compose') {
|
||||||
|
params = { prompt: paramInput };
|
||||||
|
} else if (paramAction.action === 'research') {
|
||||||
|
params = { keyword: paramInput };
|
||||||
|
} else {
|
||||||
|
try { params = JSON.parse(paramInput); } catch { params = { value: paramInput }; }
|
||||||
|
}
|
||||||
|
const result = await sendAgentCommand(agentId, paramAction.action, params);
|
||||||
|
onCommandResult?.(result);
|
||||||
|
setParamInput('');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomCommand = async () => {
|
||||||
|
if (!customAction.trim()) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
let params = {};
|
||||||
|
if (customParams.trim()) {
|
||||||
|
try { params = JSON.parse(customParams); } catch { params = { value: customParams }; }
|
||||||
|
}
|
||||||
|
const result = await sendAgentCommand(agentId, customAction, params);
|
||||||
|
onCommandResult?.(result);
|
||||||
|
setCustomAction('');
|
||||||
|
setCustomParams('');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApproval = async (approved) => {
|
||||||
|
if (!pendingTask) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await approveAgentTask(agentId, pendingTask.id, approved);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ao-command-tab">
|
||||||
|
{/* 승인 대기 UI */}
|
||||||
|
{agentState === 'waiting' && 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>
|
||||||
|
<div className="ao-approval-actions">
|
||||||
|
<button className="ao-btn-approve" onClick={() => handleApproval(true)} disabled={loading}>Approve</button>
|
||||||
|
<button className="ao-btn-reject" onClick={() => handleApproval(false)} disabled={loading}>Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<div className="ao-section">
|
||||||
|
<div className="ao-section-label">Quick Actions</div>
|
||||||
|
<div className="ao-quick-actions">
|
||||||
|
{quickActions.map(qa => (
|
||||||
|
<button
|
||||||
|
key={qa.action}
|
||||||
|
className="ao-btn-quick"
|
||||||
|
onClick={() => handleQuickAction(qa.action)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{qa.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Parameterized Action */}
|
||||||
|
{paramAction && (
|
||||||
|
<div className="ao-section">
|
||||||
|
<div className="ao-section-label">{paramAction.label}</div>
|
||||||
|
<div className="ao-param-row">
|
||||||
|
<input
|
||||||
|
className="ao-input"
|
||||||
|
value={paramInput}
|
||||||
|
onChange={e => setParamInput(e.target.value)}
|
||||||
|
placeholder={paramAction.placeholder}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && handleParamAction()}
|
||||||
|
/>
|
||||||
|
<button className="ao-btn-send" onClick={handleParamAction} disabled={loading || !paramInput.trim()}>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom Command */}
|
||||||
|
<div className="ao-section">
|
||||||
|
<div className="ao-section-label">Custom Command</div>
|
||||||
|
<input
|
||||||
|
className="ao-input"
|
||||||
|
value={customAction}
|
||||||
|
onChange={e => setCustomAction(e.target.value)}
|
||||||
|
placeholder="Action name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="ao-input"
|
||||||
|
value={customParams}
|
||||||
|
onChange={e => setCustomParams(e.target.value)}
|
||||||
|
placeholder='Parameters (JSON)'
|
||||||
|
style={{ marginTop: 4 }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="ao-btn-send"
|
||||||
|
onClick={handleCustomCommand}
|
||||||
|
disabled={loading || !customAction.trim()}
|
||||||
|
style={{ marginTop: 4, width: '100%' }}
|
||||||
|
>
|
||||||
|
Send Command
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user