Add per-agent skill workspaces and TraderView management

This commit is contained in:
2026-03-17 13:55:14 +08:00
parent 1f5ee3698e
commit 2daf5717ba
35 changed files with 4774 additions and 331 deletions

View File

@@ -17,6 +17,7 @@ import NetValueChart from './components/NetValueChart';
import StockLogo from './components/StockLogo';
import Header from './components/Header.jsx';
import WatchlistPanel from './components/WatchlistPanel.jsx';
import RuntimeSettingsPanel from './components/RuntimeSettingsPanel.jsx';
// Utils
import { formatNumber, formatTickerPrice } from './utils/formatters';
@@ -25,6 +26,8 @@ const RoomView = lazy(() => import('./components/RoomView'));
const AgentFeed = lazy(() => import('./components/AgentFeed'));
const StatisticsView = lazy(() => import('./components/StatisticsView'));
const StockExplainView = lazy(() => import('./components/StockExplainView.jsx'));
const TraderView = lazy(() => import('./components/TraderView.jsx'));
const EDITABLE_AGENT_WORKSPACE_FILES = ['SOUL.md', 'PROFILE.md', 'AGENTS.md', 'MEMORY.md', 'POLICY.md', 'HEARTBEAT.md', 'ROLE.md', 'STYLE.md'];
function ViewLoadingFallback({ label = '加载中...' }) {
return (
@@ -61,8 +64,8 @@ export default function LiveTradingApp() {
const [progress, setProgress] = useState({ current: 0, total: 0 });
const [now, setNow] = useState(() => new Date());
// View toggle: 'room' | 'explain' | 'chart' | 'statistics'
const [currentView, setCurrentView] = useState('chart'); // Start with chart, then animate to room
// View toggle: 'traders' | 'room' | 'explain' | 'chart' | 'statistics'
const [currentView, setCurrentView] = useState('traders');
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
const [lastUpdate, setLastUpdate] = useState(new Date());
const [isUpdating, setIsUpdating] = useState(false);
@@ -112,15 +115,38 @@ export default function LiveTradingApp() {
const [dataSources, setDataSources] = useState(null);
const [runtimeConfig, setRuntimeConfig] = useState(null);
const [isWatchlistPanelOpen, setIsWatchlistPanelOpen] = useState(false);
const [isRuntimeSettingsOpen, setIsRuntimeSettingsOpen] = useState(false);
const [watchlistDraftSymbols, setWatchlistDraftSymbols] = useState([]);
const [watchlistInputValue, setWatchlistInputValue] = useState('');
const [watchlistFeedback, setWatchlistFeedback] = useState(null);
const [isWatchlistSaving, setIsWatchlistSaving] = useState(false);
const [scheduleModeDraft, setScheduleModeDraft] = useState('daily');
const [intervalMinutesDraft, setIntervalMinutesDraft] = useState('60');
const [triggerTimeDraft, setTriggerTimeDraft] = useState('09:30');
const [maxCommCyclesDraft, setMaxCommCyclesDraft] = useState('2');
const [runtimeConfigFeedback, setRuntimeConfigFeedback] = useState(null);
const [isRuntimeConfigSaving, setIsRuntimeConfigSaving] = useState(false);
const [selectedSkillAgentId, setSelectedSkillAgentId] = useState(AGENTS[0]?.id || 'portfolio_manager');
const [agentProfilesByAgent, setAgentProfilesByAgent] = useState({});
const [agentSkillsByAgent, setAgentSkillsByAgent] = useState({});
const [skillDetailsByName, setSkillDetailsByName] = useState({});
const [localSkillDraftsByKey, setLocalSkillDraftsByKey] = useState({});
const [isAgentSkillsLoading, setIsAgentSkillsLoading] = useState(false);
const [skillDetailLoadingKey, setSkillDetailLoadingKey] = useState(null);
const [agentSkillsSavingKey, setAgentSkillsSavingKey] = useState(null);
const [agentSkillsFeedback, setAgentSkillsFeedback] = useState(null);
const [selectedWorkspaceFile, setSelectedWorkspaceFile] = useState(EDITABLE_AGENT_WORKSPACE_FILES[0]);
const [workspaceFilesByAgent, setWorkspaceFilesByAgent] = useState({});
const [workspaceDraftContent, setWorkspaceDraftContent] = useState('');
const [isWorkspaceFileLoading, setIsWorkspaceFileLoading] = useState(false);
const [workspaceFileSavingKey, setWorkspaceFileSavingKey] = useState(null);
const [workspaceFileFeedback, setWorkspaceFileFeedback] = useState(null);
const clientRef = useRef(null);
const containerRef = useRef(null);
const agentFeedRef = useRef(null);
const isWatchlistSavingRef = useRef(false);
const isRuntimeConfigSavingRef = useRef(false);
const requestedStockHistoryRef = useRef(new Set());
// Track last virtual time update to calculate increment
@@ -220,6 +246,38 @@ export default function LiveTradingApp() {
.filter((symbol) => typeof symbol === 'string' && symbol.trim());
}, [displayTickers, runtimeConfig]);
const runtimeSummaryLabel = useMemo(() => {
if (!runtimeConfig) {
return null;
}
const scheduleMode = String(runtimeConfig.schedule_mode || 'daily');
const intervalMinutes = Number(runtimeConfig.interval_minutes || 60);
const triggerTime = String(runtimeConfig.trigger_time || '09:30');
const maxCommCycles = Number(runtimeConfig.max_comm_cycles || 2);
if (scheduleMode === 'intraday') {
return `调度 intraday / ${intervalMinutes}m / 讨论 ${maxCommCycles}`;
}
return `调度 daily / ${triggerTime} ET / 讨论 ${maxCommCycles}`;
}, [runtimeConfig]);
const selectedAgentSkills = useMemo(
() => agentSkillsByAgent[selectedSkillAgentId] || [],
[agentSkillsByAgent, selectedSkillAgentId]
);
const selectedAgentProfile = useMemo(
() => agentProfilesByAgent[selectedSkillAgentId] || null,
[agentProfilesByAgent, selectedSkillAgentId]
);
const selectedWorkspaceContent = useMemo(
() => workspaceFilesByAgent[selectedSkillAgentId]?.[selectedWorkspaceFile] || '',
[selectedSkillAgentId, selectedWorkspaceFile, workspaceFilesByAgent]
);
useEffect(() => {
const symbols = displayTickers
.map((ticker) => ticker.symbol)
@@ -235,6 +293,17 @@ export default function LiveTradingApp() {
}
}, [displayTickers, selectedExplainSymbol]);
useEffect(() => {
if (!runtimeConfig) {
return;
}
setScheduleModeDraft(String(runtimeConfig.schedule_mode || 'daily'));
setIntervalMinutesDraft(String(runtimeConfig.interval_minutes || 60));
setTriggerTimeDraft(String(runtimeConfig.trigger_time || '09:30'));
setMaxCommCyclesDraft(String(runtimeConfig.max_comm_cycles || 2));
}, [runtimeConfig]);
const watchlistSuggestions = useMemo(
() => INITIAL_TICKERS.map((ticker) => ticker.symbol).filter((symbol, index, list) => list.indexOf(symbol) === index),
[]
@@ -350,6 +419,7 @@ export default function LiveTradingApp() {
}, [watchlistFeedback]);
const handleWatchlistPanelToggle = useCallback(() => {
setIsRuntimeSettingsOpen(false);
setIsWatchlistPanelOpen((open) => {
const nextOpen = !open;
if (nextOpen) {
@@ -425,6 +495,292 @@ export default function LiveTradingApp() {
}
}, [parseWatchlistInput, watchlistDraftSymbols, watchlistInputValue]);
const handleManualTrigger = useCallback(() => {
if (!clientRef.current) {
addSystemMessage('连接未就绪,无法手动触发');
return;
}
const success = clientRef.current.send({
type: 'trigger_strategy'
});
if (!success) {
addSystemMessage('手动触发发送失败,请检查连接状态');
return;
}
addSystemMessage('已发送手动触发请求');
}, [addSystemMessage]);
const handleRuntimeConfigSave = useCallback(() => {
if (!clientRef.current) {
setRuntimeConfigFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
const interval = Number(intervalMinutesDraft);
const maxCommCycles = Number(maxCommCyclesDraft);
if (!Number.isInteger(interval) || interval <= 0) {
setRuntimeConfigFeedback({ type: 'error', text: '间隔必须是正整数分钟' });
return;
}
if (!Number.isInteger(maxCommCycles) || maxCommCycles <= 0) {
setRuntimeConfigFeedback({ type: 'error', text: '讨论轮数必须是正整数' });
return;
}
setIsRuntimeConfigSaving(true);
setRuntimeConfigFeedback(null);
const success = clientRef.current.send({
type: 'update_runtime_config',
schedule_mode: scheduleModeDraft,
interval_minutes: interval,
trigger_time: triggerTimeDraft,
max_comm_cycles: maxCommCycles
});
if (!success) {
setIsRuntimeConfigSaving(false);
setRuntimeConfigFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [intervalMinutesDraft, maxCommCyclesDraft, scheduleModeDraft, triggerTimeDraft]);
const handleRuntimeDefaultsRestore = useCallback(() => {
setScheduleModeDraft('daily');
setIntervalMinutesDraft('60');
setTriggerTimeDraft('09:30');
setMaxCommCyclesDraft('2');
setRuntimeConfigFeedback(null);
}, []);
const handleRuntimeSettingsToggle = useCallback(() => {
setRuntimeConfigFeedback(null);
setAgentSkillsFeedback(null);
setWorkspaceFileFeedback(null);
setIsRuntimeSettingsOpen((prev) => !prev);
setIsWatchlistPanelOpen(false);
}, []);
const requestAgentSkills = useCallback((agentId) => {
const normalized = typeof agentId === 'string' ? agentId.trim() : '';
if (!normalized || !clientRef.current) {
return false;
}
setIsAgentSkillsLoading(true);
setAgentSkillsFeedback(null);
return clientRef.current.send({
type: 'get_agent_skills',
agent_id: normalized
});
}, []);
const requestAgentProfile = useCallback((agentId) => {
const normalized = typeof agentId === 'string' ? agentId.trim() : '';
if (!normalized || !clientRef.current) {
return false;
}
return clientRef.current.send({
type: 'get_agent_profile',
agent_id: normalized
});
}, []);
const requestSkillDetail = useCallback((skillName) => {
const normalized = typeof skillName === 'string' ? skillName.trim() : '';
if (!normalized || !clientRef.current) {
return false;
}
const detailKey = `${selectedSkillAgentId}:${normalized}`;
setSkillDetailLoadingKey(detailKey);
return clientRef.current.send({
type: 'get_skill_detail',
agent_id: selectedSkillAgentId,
skill_name: normalized
});
}, [selectedSkillAgentId]);
const handleCreateLocalSkill = useCallback((skillName) => {
const normalized = typeof skillName === 'string' ? skillName.trim() : '';
if (!normalized) {
setAgentSkillsFeedback({ type: 'error', text: '技能名称不能为空' });
return;
}
if (!clientRef.current) {
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${normalized}:create`);
setAgentSkillsFeedback(null);
const success = clientRef.current.send({
type: 'create_agent_local_skill',
agent_id: selectedSkillAgentId,
skill_name: normalized
});
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [selectedSkillAgentId]);
const handleLocalSkillDraftChange = useCallback((skillName, content) => {
const detailKey = `${selectedSkillAgentId}:${skillName}`;
setLocalSkillDraftsByKey((prev) => ({
...prev,
[detailKey]: content
}));
}, [selectedSkillAgentId]);
const handleLocalSkillSave = useCallback((skillName) => {
if (!clientRef.current) {
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
const detailKey = `${selectedSkillAgentId}:${skillName}`;
const content = localSkillDraftsByKey[detailKey];
if (typeof content !== 'string') {
return;
}
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:content`);
setAgentSkillsFeedback(null);
const success = clientRef.current.send({
type: 'update_agent_local_skill',
agent_id: selectedSkillAgentId,
skill_name: skillName,
content
});
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [localSkillDraftsByKey, selectedSkillAgentId]);
const handleLocalSkillDelete = useCallback((skillName) => {
if (!clientRef.current) {
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:delete`);
setAgentSkillsFeedback(null);
const success = clientRef.current.send({
type: 'delete_agent_local_skill',
agent_id: selectedSkillAgentId,
skill_name: skillName
});
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [selectedSkillAgentId]);
const handleRemoveSharedSkill = useCallback((skillName) => {
if (!clientRef.current) {
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
setAgentSkillsSavingKey(`${selectedSkillAgentId}:${skillName}:remove`);
setAgentSkillsFeedback(null);
const success = clientRef.current.send({
type: 'remove_agent_skill',
agent_id: selectedSkillAgentId,
skill_name: skillName
});
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [selectedSkillAgentId]);
const requestWorkspaceFile = useCallback((agentId, filename) => {
const normalizedAgentId = typeof agentId === 'string' ? agentId.trim() : '';
const normalizedFilename = typeof filename === 'string' ? filename.trim() : '';
if (!normalizedAgentId || !normalizedFilename || !clientRef.current) {
return false;
}
setIsWorkspaceFileLoading(true);
setWorkspaceFileFeedback(null);
return clientRef.current.send({
type: 'get_agent_workspace_file',
agent_id: normalizedAgentId,
filename: normalizedFilename
});
}, []);
const handleAgentSkillToggle = useCallback((skillName, enabled) => {
if (!clientRef.current) {
setAgentSkillsFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
const agentId = selectedSkillAgentId;
setAgentSkillsSavingKey(`${agentId}:${skillName}`);
setAgentSkillsFeedback(null);
const success = clientRef.current.send({
type: 'update_agent_skill',
agent_id: agentId,
skill_name: skillName,
enabled
});
if (!success) {
setAgentSkillsSavingKey(null);
setAgentSkillsFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [selectedSkillAgentId]);
const handleSkillAgentChange = useCallback((agentId) => {
setSelectedSkillAgentId(agentId);
requestAgentProfile(agentId);
requestAgentSkills(agentId);
requestWorkspaceFile(agentId, selectedWorkspaceFile);
}, [requestAgentProfile, requestAgentSkills, requestWorkspaceFile, selectedWorkspaceFile]);
const handleWorkspaceFileChange = useCallback((filename) => {
setSelectedWorkspaceFile(filename);
requestWorkspaceFile(selectedSkillAgentId, filename);
}, [requestWorkspaceFile, selectedSkillAgentId]);
const handleWorkspaceFileSave = useCallback(() => {
if (!clientRef.current) {
setWorkspaceFileFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
return;
}
const key = `${selectedSkillAgentId}:${selectedWorkspaceFile}`;
setWorkspaceFileSavingKey(key);
setWorkspaceFileFeedback(null);
const success = clientRef.current.send({
type: 'update_agent_workspace_file',
agent_id: selectedSkillAgentId,
filename: selectedWorkspaceFile,
content: workspaceDraftContent
});
if (!success) {
setWorkspaceFileSavingKey(null);
setWorkspaceFileFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
}
}, [selectedSkillAgentId, selectedWorkspaceFile, workspaceDraftContent]);
useEffect(() => {
setWorkspaceDraftContent(selectedWorkspaceContent);
}, [selectedWorkspaceContent]);
useEffect(() => {
if (currentView !== 'traders' || !isConnected) {
return;
}
AGENTS.forEach((agent) => {
if (!agentProfilesByAgent[agent.id]) {
requestAgentProfile(agent.id);
}
if (!agentSkillsByAgent[agent.id]) {
requestAgentSkills(agent.id);
}
if (!workspaceFilesByAgent[agent.id]?.['MEMORY.md']) {
requestWorkspaceFile(agent.id, 'MEMORY.md');
}
});
}, [agentProfilesByAgent, agentSkillsByAgent, currentView, isConnected, requestAgentProfile, requestAgentSkills, requestWorkspaceFile, workspaceFilesByAgent]);
const requestStockHistory = useCallback((symbol, { force = false } = {}) => {
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
if (!normalized || !clientRef.current) {
@@ -604,6 +960,10 @@ export default function LiveTradingApp() {
isWatchlistSavingRef.current = isWatchlistSaving;
}, [isWatchlistSaving]);
useEffect(() => {
isRuntimeConfigSavingRef.current = isRuntimeConfigSaving;
}, [isRuntimeConfigSaving]);
useEffect(() => {
if (currentView !== 'explain' || !selectedExplainSymbol) {
return;
@@ -670,24 +1030,18 @@ export default function LiveTradingApp() {
return () => clearTimeout(timer);
}, [holdings, stats, trades, portfolioData.netValue]);
// Initial animation: show room drawer sliding in
// Initial animation flag for slider speed
useEffect(() => {
// Wait a bit after mount, then trigger slide to room
const slideTimer = setTimeout(() => {
setCurrentView('room');
}, 1200); // Wait 1200ms before starting animation (2x slower)
// Disable animation flag after animation completes
const completeTimer = setTimeout(() => {
setIsInitialAnimating(false);
}, 5000); // 1200ms delay + 1600ms animation duration + 400ms buffer
}, 1800);
return () => {
clearTimeout(slideTimer);
clearTimeout(completeTimer);
};
}, []);
// Helper to check if bubble should still be visible
// Bubbles persist until replaced by ANY new message (cross-role)
// When any agent sends a new message, all previous bubbles are cleared
@@ -769,21 +1123,38 @@ export default function LiveTradingApp() {
const handlers = {
// Error response (for fast forward errors)
error: (e) => {
console.error('[Error]', e.message);
const message = typeof e.message === 'string' ? e.message : '请求失败';
console.error('[Error]', message);
setIsAgentSkillsLoading(false);
setSkillDetailLoadingKey(null);
setAgentSkillsSavingKey(null);
setIsWorkspaceFileLoading(false);
setWorkspaceFileSavingKey(null);
if (isWatchlistSavingRef.current) {
setIsWatchlistSaving(false);
setWatchlistFeedback({ type: 'error', text: e.message || '更新 watchlist 失败' });
setWatchlistFeedback({ type: 'error', text: message || '更新 watchlist 失败' });
}
if (isRuntimeConfigSavingRef.current) {
setIsRuntimeConfigSaving(false);
setRuntimeConfigFeedback({ type: 'error', text: message });
}
if (message.includes('skill') || message.includes('agent_id')) {
setAgentSkillsFeedback({ type: 'error', text: message || '更新技能失败' });
}
if (message.includes('workspace_file') || message.includes('filename')) {
setWorkspaceFileFeedback({ type: 'error', text: message || '更新工作区文件失败' });
}
// Handle fast forward errors
if (e.message && e.message.includes('fast forward')) {
console.warn(`⚠️ ${e.message}`);
if (message.includes('fast forward')) {
console.warn(`⚠️ ${message}`);
handlePushEvent({
type: 'system',
content: `⚠️ ${e.message}`,
content: `⚠️ ${message}`,
timestamp: Date.now()
});
}
addSystemMessage(message);
},
// Connection events
@@ -930,9 +1301,163 @@ export default function LiveTradingApp() {
if (isWatchlistSavingRef.current) {
setIsWatchlistSaving(false);
}
if (isRuntimeConfigSavingRef.current) {
setIsRuntimeConfigSaving(false);
setRuntimeConfigFeedback({ type: 'success', text: '运行配置已保存并生效' });
}
const warnings = Array.isArray(e.runtime_config_warnings) ? e.runtime_config_warnings : [];
warnings.forEach((warning) => addSystemMessage(warning));
addSystemMessage('运行时配置已热更新');
},
agent_skills_loaded: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
if (!agentId) {
setIsAgentSkillsLoading(false);
return;
}
setAgentSkillsByAgent((prev) => ({
...prev,
[agentId]: Array.isArray(e.skills) ? e.skills : []
}));
setIsAgentSkillsLoading(false);
setAgentSkillsSavingKey(null);
},
agent_profile_loaded: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
if (!agentId) {
return;
}
setAgentProfilesByAgent((prev) => ({
...prev,
[agentId]: e.profile && typeof e.profile === 'object' ? e.profile : {}
}));
},
skill_detail_loaded: (e) => {
const skillName = typeof e.skill?.skill_name === 'string' ? e.skill.skill_name.trim() : '';
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : selectedSkillAgentId;
if (!skillName) {
setSkillDetailLoadingKey(null);
return;
}
const detailKey = `${agentId}:${skillName}`;
setSkillDetailsByName((prev) => ({
...prev,
[detailKey]: e.skill
}));
setLocalSkillDraftsByKey((prev) => ({
...prev,
[detailKey]: typeof e.skill?.content === 'string' ? e.skill.content : ''
}));
setSkillDetailLoadingKey(null);
},
agent_skill_updated: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : '';
if (!agentId || !skillName) {
return;
}
setAgentSkillsFeedback({
type: 'success',
text: `${agentId} ${e.enabled ? '已启用' : '已禁用'} ${skillName}`
});
},
agent_local_skill_created: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : '';
setAgentSkillsSavingKey(null);
if (!agentId || !skillName) {
return;
}
setAgentSkillsFeedback({
type: 'success',
text: `${agentId} 已创建本地技能 ${skillName}`
});
},
agent_local_skill_updated: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : '';
setAgentSkillsSavingKey(null);
if (!agentId || !skillName) {
return;
}
setAgentSkillsFeedback({
type: 'success',
text: `${agentId} 的本地技能 ${skillName} 已保存`
});
},
agent_local_skill_deleted: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : '';
setAgentSkillsSavingKey(null);
if (!agentId || !skillName) {
return;
}
setSkillDetailsByName((prev) => {
const next = { ...prev };
delete next[`${agentId}:${skillName}`];
return next;
});
setLocalSkillDraftsByKey((prev) => {
const next = { ...prev };
delete next[`${agentId}:${skillName}`];
return next;
});
setAgentSkillsFeedback({
type: 'success',
text: `${agentId} 的本地技能 ${skillName} 已删除`
});
},
agent_skill_removed: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const skillName = typeof e.skill_name === 'string' ? e.skill_name.trim() : '';
setAgentSkillsSavingKey(null);
if (!agentId || !skillName) {
return;
}
setAgentSkillsFeedback({
type: 'success',
text: `${agentId} 已移除共享技能 ${skillName}`
});
},
agent_workspace_file_loaded: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const filename = typeof e.filename === 'string' ? e.filename.trim() : '';
if (!agentId || !filename) {
setIsWorkspaceFileLoading(false);
return;
}
setWorkspaceFilesByAgent((prev) => ({
...prev,
[agentId]: {
...(prev[agentId] || {}),
[filename]: typeof e.content === 'string' ? e.content : ''
}
}));
setIsWorkspaceFileLoading(false);
setWorkspaceFileSavingKey(null);
},
agent_workspace_file_updated: (e) => {
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
const filename = typeof e.filename === 'string' ? e.filename.trim() : '';
if (!agentId || !filename) {
return;
}
setWorkspaceFileFeedback({
type: 'success',
text: `${agentId}${filename} 已保存`
});
},
watchlist_updated: (e) => {
if (Array.isArray(e.tickers)) {
const normalizedTickers = e.tickers.map((symbol) => String(symbol).trim().toUpperCase());
@@ -1713,10 +2238,41 @@ export default function LiveTradingApp() {
</span>
</>
)}
{runtimeSummaryLabel && (
<>
<span className="status-sep">·</span>
<span className="market-text backtest" title="当前运行配置">
{runtimeSummaryLabel}
</span>
</>
)}
<span className="status-sep">·</span>
<span className="time-text">{now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</span>
</div>
{serverMode !== 'backtest' && (
<button
onClick={handleManualTrigger}
disabled={!isConnected}
style={{
padding: '6px 12px',
borderRadius: 4,
background: isConnected ? '#111111' : '#8a8a8a',
border: '1px solid #111111',
color: '#FFFFFF',
fontSize: '11px',
fontFamily: '"Courier New", monospace',
fontWeight: 700,
cursor: isConnected ? 'pointer' : 'not-allowed',
letterSpacing: '0.4px',
textTransform: 'uppercase'
}}
title="手动触发一轮分析与交易决策"
>
手动运行
</button>
)}
<WatchlistPanel
isOpen={isWatchlistPanelOpen}
isConnected={isConnected}
@@ -1736,6 +2292,26 @@ export default function LiveTradingApp() {
onSuggestionClick={handleWatchlistSuggestionClick}
onSave={handleWatchlistSave}
/>
<RuntimeSettingsPanel
isOpen={isRuntimeSettingsOpen}
isConnected={isConnected}
isSaving={isRuntimeConfigSaving}
feedback={runtimeConfigFeedback}
runtimeConfig={runtimeConfig}
scheduleMode={scheduleModeDraft}
intervalMinutes={intervalMinutesDraft}
triggerTime={triggerTimeDraft}
maxCommCycles={maxCommCyclesDraft}
onToggle={handleRuntimeSettingsToggle}
onClose={() => setIsRuntimeSettingsOpen(false)}
onScheduleModeChange={setScheduleModeDraft}
onIntervalMinutesChange={setIntervalMinutesDraft}
onTriggerTimeChange={setTriggerTimeDraft}
onMaxCommCyclesChange={setMaxCommCyclesDraft}
onSave={handleRuntimeConfigSave}
onRestoreDefaults={handleRuntimeDefaultsRestore}
/>
</div>
</div>
@@ -1783,6 +2359,13 @@ export default function LiveTradingApp() {
<div className="chart-section">
<div className="view-container">
<div className="view-nav-bar">
<button
className={`view-nav-btn ${currentView === 'traders' ? 'active' : ''}`}
onClick={() => setCurrentView('traders')}
>
交易员
</button>
<button
className={`view-nav-btn ${currentView === 'room' ? 'active' : ''}`}
onClick={() => setCurrentView('room')}
@@ -1812,9 +2395,10 @@ export default function LiveTradingApp() {
</button>
</div>
{/* Slider container with four views */}
<div className={`view-slider-four ${
currentView === 'room'
<div className={`view-slider-five ${
currentView === 'traders'
? 'show-traders'
: currentView === 'room'
? 'show-room'
: currentView === 'explain'
? 'show-explain'
@@ -1822,6 +2406,45 @@ export default function LiveTradingApp() {
? 'show-statistics'
: 'show-chart'
} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
<div className="view-panel">
<Suspense fallback={<ViewLoadingFallback label="加载交易员视图..." />}>
<TraderView
agents={AGENTS}
agentProfilesByAgent={agentProfilesByAgent}
agentSkillsByAgent={agentSkillsByAgent}
workspaceFilesByAgent={workspaceFilesByAgent}
selectedAgentId={selectedSkillAgentId}
selectedAgentProfile={selectedAgentProfile}
selectedAgentSkills={selectedAgentSkills}
skillDetailsByName={skillDetailsByName}
localSkillDraftsByKey={localSkillDraftsByKey}
skillDetailLoadingKey={skillDetailLoadingKey}
editableFiles={EDITABLE_AGENT_WORKSPACE_FILES}
selectedWorkspaceFile={selectedWorkspaceFile}
workspaceFileContent={selectedWorkspaceContent}
workspaceDraftContent={workspaceDraftContent}
isConnected={isConnected}
isAgentSkillsLoading={isAgentSkillsLoading}
agentSkillsSavingKey={agentSkillsSavingKey}
agentSkillsFeedback={agentSkillsFeedback}
isWorkspaceFileLoading={isWorkspaceFileLoading}
workspaceFileSavingKey={workspaceFileSavingKey}
workspaceFileFeedback={workspaceFileFeedback}
onAgentChange={handleSkillAgentChange}
onCreateLocalSkill={handleCreateLocalSkill}
onSkillDetailRequest={requestSkillDetail}
onLocalSkillDraftChange={handleLocalSkillDraftChange}
onLocalSkillDelete={handleLocalSkillDelete}
onLocalSkillSave={handleLocalSkillSave}
onRemoveSharedSkill={handleRemoveSharedSkill}
onSkillToggle={handleAgentSkillToggle}
onWorkspaceFileChange={handleWorkspaceFileChange}
onWorkspaceDraftChange={setWorkspaceDraftContent}
onWorkspaceFileSave={handleWorkspaceFileSave}
/>
</Suspense>
</div>
{/* Room View Panel */}
<div className="view-panel">
<Suspense fallback={<ViewLoadingFallback label="加载交易室..." />}>