Add per-agent skill workspaces and TraderView management
This commit is contained in:
@@ -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="加载交易室..." />}>
|
||||
|
||||
247
frontend/src/components/RuntimeSettingsPanel.jsx
Normal file
247
frontend/src/components/RuntimeSettingsPanel.jsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function RuntimeSettingsPanel({
|
||||
isOpen,
|
||||
isConnected,
|
||||
isSaving,
|
||||
feedback,
|
||||
runtimeConfig,
|
||||
scheduleMode,
|
||||
intervalMinutes,
|
||||
triggerTime,
|
||||
maxCommCycles,
|
||||
onToggle,
|
||||
onClose,
|
||||
onScheduleModeChange,
|
||||
onIntervalMinutesChange,
|
||||
onTriggerTimeChange,
|
||||
onMaxCommCyclesChange,
|
||||
onSave,
|
||||
onRestoreDefaults
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, position: 'relative' }}>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
borderRadius: 4,
|
||||
border: '1px solid #333333',
|
||||
background: isOpen ? '#1E1E1E' : '#111111',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.6px',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
运行设置
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(100% + 10px)',
|
||||
right: 0,
|
||||
width: 320,
|
||||
maxWidth: 'min(320px, 92vw)',
|
||||
padding: '14px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D9D9D9',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 12px 36px rgba(0, 0, 0, 0.14)',
|
||||
zIndex: 40,
|
||||
display: 'grid',
|
||||
gap: 12
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', fontWeight: 700, color: '#111111', letterSpacing: '0.3px' }}>
|
||||
运行设置
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#666666', marginTop: 2 }}>
|
||||
保存后立即热更新当前运行中的调度参数
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: '#666666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>调度模式</span>
|
||||
<select
|
||||
value={scheduleMode}
|
||||
onChange={(e) => onScheduleModeChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value="daily">daily</option>
|
||||
<option value="intraday">intraday</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>间隔(分钟)</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={intervalMinutes}
|
||||
onChange={(e) => onIntervalMinutesChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>Daily 时间 (NYSE)</span>
|
||||
<input
|
||||
type="time"
|
||||
value={triggerTime}
|
||||
onChange={(e) => onTriggerTimeChange(e.target.value)}
|
||||
disabled={scheduleMode !== 'daily'}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: scheduleMode === 'daily' ? '#FFFFFF' : '#F3F4F6',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
<span style={{ fontSize: '10px', color: '#4B5563', fontWeight: 700 }}>讨论轮数上限</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={maxCommCycles}
|
||||
onChange={(e) => onMaxCommCyclesChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
|
||||
<button
|
||||
onClick={onRestoreDefaults}
|
||||
style={{
|
||||
padding: '9px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={!isConnected || isSaving}
|
||||
style={{
|
||||
padding: '9px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !isSaving ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.4px',
|
||||
cursor: isConnected && !isSaving ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{isSaving ? '保存中' : '保存运行配置'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{feedback && (
|
||||
<span style={{
|
||||
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: '11px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{feedback.text}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{runtimeConfig && (
|
||||
<div style={{
|
||||
borderTop: '1px solid #E5E7EB',
|
||||
paddingTop: 12,
|
||||
display: 'grid',
|
||||
gap: 8
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', fontWeight: 700, color: '#111111', letterSpacing: '0.3px' }}>
|
||||
当前生效配置
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#666666', marginTop: 2 }}>
|
||||
这里显示当前 run 已加载并生效的参数
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#F8FAFC',
|
||||
borderRadius: 6,
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 6,
|
||||
fontSize: '11px',
|
||||
fontFamily: '"Courier New", monospace',
|
||||
color: '#111111'
|
||||
}}>
|
||||
<div>tickers: {(runtimeConfig.tickers || []).join(', ') || '-'}</div>
|
||||
<div>schedule_mode: {runtimeConfig.schedule_mode || '-'}</div>
|
||||
<div>interval_minutes: {runtimeConfig.interval_minutes ?? '-'}</div>
|
||||
<div>trigger_time: {runtimeConfig.trigger_time || '-'}</div>
|
||||
<div>max_comm_cycles: {runtimeConfig.max_comm_cycles ?? '-'}</div>
|
||||
<div>initial_cash: {runtimeConfig.initial_cash ?? '-'}</div>
|
||||
<div>margin_requirement: {runtimeConfig.margin_requirement ?? '-'}</div>
|
||||
<div>enable_memory: {String(runtimeConfig.enable_memory ?? false)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
765
frontend/src/components/TraderView.jsx
Normal file
765
frontend/src/components/TraderView.jsx
Normal file
@@ -0,0 +1,765 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
|
||||
|
||||
export default function TraderView({
|
||||
agents,
|
||||
agentProfilesByAgent,
|
||||
agentSkillsByAgent,
|
||||
workspaceFilesByAgent,
|
||||
selectedAgentId,
|
||||
selectedAgentProfile,
|
||||
selectedAgentSkills,
|
||||
skillDetailsByName,
|
||||
localSkillDraftsByKey,
|
||||
skillDetailLoadingKey,
|
||||
editableFiles,
|
||||
selectedWorkspaceFile,
|
||||
workspaceFileContent,
|
||||
workspaceDraftContent,
|
||||
isConnected,
|
||||
isAgentSkillsLoading,
|
||||
agentSkillsSavingKey,
|
||||
agentSkillsFeedback,
|
||||
isWorkspaceFileLoading,
|
||||
workspaceFileSavingKey,
|
||||
workspaceFileFeedback,
|
||||
onAgentChange,
|
||||
onCreateLocalSkill,
|
||||
onSkillDetailRequest,
|
||||
onLocalSkillDraftChange,
|
||||
onLocalSkillDelete,
|
||||
onLocalSkillSave,
|
||||
onRemoveSharedSkill,
|
||||
onSkillToggle,
|
||||
onWorkspaceFileChange,
|
||||
onWorkspaceDraftChange,
|
||||
onWorkspaceFileSave
|
||||
}) {
|
||||
const [expandedSkillKey, setExpandedSkillKey] = useState(null);
|
||||
const [newLocalSkillName, setNewLocalSkillName] = useState('');
|
||||
const [isSkillPickerOpen, setIsSkillPickerOpen] = useState(false);
|
||||
|
||||
const selectedAgent = useMemo(
|
||||
() => agents.find((agent) => agent.id === selectedAgentId) || agents[0] || null,
|
||||
[agents, selectedAgentId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedSkillKey(null);
|
||||
}, [selectedAgentId]);
|
||||
|
||||
if (!selectedAgent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = selectedAgentProfile || {};
|
||||
const modelInfo = getModelIcon(profile.model_name, profile.model_provider);
|
||||
const activeSkills = selectedAgentSkills.filter((item) => item.status === 'enabled' || item.status === 'active');
|
||||
const installedSkills = selectedAgentSkills.filter((item) => item.status !== 'available');
|
||||
const availableSkills = selectedAgentSkills.filter((item) => item.status === 'available');
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
padding: '18px',
|
||||
background: 'linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%)',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||
gap: 18
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.5px', color: '#111111' }}>
|
||||
交易员档案
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
聚焦查看每个 Agent 的模型、工具组、技能编排和工作区记忆,不展示交易表现数据
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '120px minmax(0, 1fr)',
|
||||
gap: 16,
|
||||
alignItems: 'stretch',
|
||||
minHeight: 0
|
||||
}}>
|
||||
<div style={{
|
||||
border: '1px solid #D9E0E7',
|
||||
borderRadius: 14,
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
|
||||
padding: 12,
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
alignContent: 'start'
|
||||
}}>
|
||||
{agents.map((agent) => {
|
||||
const isSelected = agent.id === selectedAgentId;
|
||||
return (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => onAgentChange(agent.id)}
|
||||
title={agent.name}
|
||||
style={{
|
||||
border: isSelected ? `2px solid ${agent.colors.accent}` : '1px solid #D9E0E7',
|
||||
borderRadius: 16,
|
||||
background: isSelected ? `${agent.colors.accent}10` : '#FFFFFF',
|
||||
boxShadow: isSelected ? `0 10px 20px ${agent.colors.accent}18` : 'none',
|
||||
padding: 8,
|
||||
display: 'grid',
|
||||
gap: 6,
|
||||
justifyItems: 'center',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={agent.avatar}
|
||||
alt={agent.name}
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
objectFit: 'cover',
|
||||
border: `1px solid ${agent.colors.accent}33`
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 800,
|
||||
color: isSelected ? agent.colors.accent : '#374151',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.4
|
||||
}}>
|
||||
{agent.name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #D9E0E7',
|
||||
borderRadius: 14,
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
|
||||
padding: 18,
|
||||
display: 'grid',
|
||||
gap: 16,
|
||||
minHeight: 0,
|
||||
overflowY: 'auto',
|
||||
alignContent: 'start'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<img
|
||||
src={selectedAgent.avatar}
|
||||
alt={selectedAgent.name}
|
||||
style={{
|
||||
width: 58,
|
||||
height: 58,
|
||||
borderRadius: 12,
|
||||
objectFit: 'cover',
|
||||
border: `1px solid ${selectedAgent.colors.accent}33`
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: '#111111' }}>{selectedAgent.name}</div>
|
||||
<div style={{ fontSize: 12, color: '#6B7280' }}>{selectedAgent.role}</div>
|
||||
<div style={{ fontSize: 11, color: selectedAgent.colors.accent, fontWeight: 700 }}>
|
||||
当前档案已展开
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: `1px solid ${modelInfo.color}2e`,
|
||||
background: modelInfo.bgColor,
|
||||
borderRadius: 12,
|
||||
padding: '10px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10
|
||||
}}>
|
||||
{modelInfo.logoPath && (
|
||||
<img
|
||||
src={modelInfo.logoPath}
|
||||
alt={modelInfo.provider}
|
||||
style={{ width: 26, height: 26, borderRadius: 999 }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'grid', gap: 2 }}>
|
||||
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
|
||||
<div style={{ fontSize: 12, color: '#111111', fontWeight: 800 }}>
|
||||
{getShortModelName(profile.model_name)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(300px, 420px) minmax(0, 1fr)',
|
||||
gap: 16,
|
||||
alignItems: 'start'
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }}>
|
||||
<div style={{ display: 'grid', gap: 2 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>技能</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
|
||||
已启用: {activeSkills.length} / 已安装: {installedSkills.length}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSkillPickerOpen(true)}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: '#EFF6FF',
|
||||
color: '#1565C0',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
aria-label="管理技能"
|
||||
>
|
||||
⚙ 技能管理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#F8FAFC',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
maxHeight: 520,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{isAgentSkillsLoading ? (
|
||||
<div style={{ fontSize: 11, color: '#666666', fontFamily: '"Courier New", monospace' }}>加载技能中...</div>
|
||||
) : installedSkills.length === 0 ? (
|
||||
<div style={{ fontSize: 11, color: '#666666', fontFamily: '"Courier New", monospace' }}>暂无技能</div>
|
||||
) : installedSkills.map((skill) => {
|
||||
const isEnabled = skill.status === 'enabled' || skill.status === 'active';
|
||||
const saving = agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:content` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:delete` || agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}:remove`;
|
||||
const isExpanded = expandedSkillKey === skill.skill_name;
|
||||
const detailKey = `${selectedAgentId}:${skill.skill_name}`;
|
||||
const skillDetail = skillDetailsByName?.[detailKey] || null;
|
||||
const skillDraft = localSkillDraftsByKey?.[detailKey] ?? '';
|
||||
const isDetailLoading = skillDetailLoadingKey === detailKey;
|
||||
const isLocalSkill = skill.source === 'local';
|
||||
return (
|
||||
<div
|
||||
key={skill.skill_name}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: 7,
|
||||
paddingBottom: 10,
|
||||
borderBottom: '1px dashed #D7DEE7'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'flex-start' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isExpanded && !skillDetail && onSkillDetailRequest) {
|
||||
onSkillDetailRequest(skill.skill_name);
|
||||
}
|
||||
setExpandedSkillKey((prev) => (prev === skill.skill_name ? null : skill.skill_name));
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
padding: 0,
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
display: 'grid',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, color: '#6B7280', fontWeight: 700 }}>
|
||||
{isExpanded ? '▾' : '▸'}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>
|
||||
{skill.name || '未命名技能'}
|
||||
</span>
|
||||
<span style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${isLocalSkill ? selectedAgent.colors.accent : '#D0D7DE'}`,
|
||||
color: isLocalSkill ? selectedAgent.colors.accent : '#6B7280',
|
||||
fontSize: 9,
|
||||
fontWeight: 700
|
||||
}}>
|
||||
{isLocalSkill ? '本地' : '共享'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#4B5563', marginLeft: 20 }}>
|
||||
{skill.description || '-'}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', marginLeft: 20 }}>
|
||||
{isExpanded ? '点击收起详情' : '点击展开详情'}
|
||||
</div>
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSkillToggle(skill.skill_name, !isEnabled)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${isEnabled ? '#C62828' : '#1565C0'}`,
|
||||
background: isConnected && !saving ? (isEnabled ? '#FFF5F5' : '#EFF6FF') : '#E5E7EB',
|
||||
color: isEnabled ? '#C62828' : '#1565C0',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : isEnabled ? '禁用' : '启用'}
|
||||
</button>
|
||||
{isLocalSkill ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onLocalSkillDelete(skill.skill_name)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #C62828',
|
||||
background: isConnected && !saving ? '#FFF5F5' : '#E5E7EB',
|
||||
color: '#C62828',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : '删除'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveSharedSkill(skill.skill_name)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #C62828',
|
||||
background: isConnected && !saving ? '#FFF5F5' : '#E5E7EB',
|
||||
color: '#C62828',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : '移除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div style={{
|
||||
marginLeft: 20,
|
||||
borderRadius: 8,
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#FFFFFF',
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 8
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 11,
|
||||
color: '#1F2937',
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{isDetailLoading
|
||||
? '加载技能说明中...'
|
||||
: (skillDetail?.content || '暂无更详细的技能说明')}
|
||||
</div>
|
||||
{isLocalSkill && !isDetailLoading && (
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', fontWeight: 700 }}>
|
||||
本地技能 SKILL.md
|
||||
</div>
|
||||
<textarea
|
||||
value={skillDraft}
|
||||
onChange={(e) => onLocalSkillDraftChange(skill.skill_name, e.target.value)}
|
||||
style={{
|
||||
minHeight: 220,
|
||||
resize: 'vertical',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
padding: '10px 12px',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.6,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onLocalSkillSave(skill.skill_name)}
|
||||
disabled={!isConnected || saving || skillDraft === (skillDetail?.content || '')}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !saving && skillDraft !== (skillDetail?.content || '') ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving && skillDraft !== (skillDetail?.content || '') ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{saving ? '保存中' : '保存本地技能'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{agentSkillsFeedback && (
|
||||
<span style={{
|
||||
color: agentSkillsFeedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{agentSkillsFeedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>工作区文件编辑</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
直接调整该交易员的人设、协作方式和长期记忆文件
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{editableFiles.map((filename) => {
|
||||
const isActive = filename === selectedWorkspaceFile;
|
||||
return (
|
||||
<button
|
||||
key={filename}
|
||||
onClick={() => onWorkspaceFileChange(filename)}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${isActive ? selectedAgent.colors.accent : '#D0D7DE'}`,
|
||||
background: isActive ? `${selectedAgent.colors.accent}12` : '#FFFFFF',
|
||||
color: isActive ? selectedAgent.colors.accent : '#4B5563',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
>
|
||||
{filename}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={workspaceDraftContent}
|
||||
onChange={(e) => onWorkspaceDraftChange(e.target.value)}
|
||||
placeholder={isWorkspaceFileLoading ? '加载中...' : '输入 markdown 内容'}
|
||||
style={{
|
||||
minHeight: 280,
|
||||
resize: 'vertical',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
padding: '12px 14px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>
|
||||
当前文件: {selectedWorkspaceFile}
|
||||
</span>
|
||||
<button
|
||||
onClick={onWorkspaceFileSave}
|
||||
disabled={!isConnected || isWorkspaceFileLoading || workspaceFileSavingKey !== null || workspaceDraftContent === workspaceFileContent}
|
||||
style={{
|
||||
padding: '9px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? '#0D47A1' : '#94A3B8',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !isWorkspaceFileLoading && workspaceFileSavingKey === null && workspaceDraftContent !== workspaceFileContent ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{workspaceFileSavingKey ? '保存中' : '保存文件'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{workspaceFileFeedback && (
|
||||
<span style={{
|
||||
color: workspaceFileFeedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{workspaceFileFeedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSkillPickerOpen && createPortal((
|
||||
<div
|
||||
onClick={() => setIsSkillPickerOpen(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(15, 23, 42, 0.28)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
zIndex: 9998
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: 'min(760px, 92vw)',
|
||||
maxHeight: '80vh',
|
||||
overflowY: 'auto',
|
||||
borderRadius: 16,
|
||||
border: '1px solid #D9E0E7',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 24px 60px rgba(15, 23, 42, 0.18)',
|
||||
padding: 18,
|
||||
paddingTop: 22,
|
||||
display: 'grid',
|
||||
gap: 16,
|
||||
position: 'relative',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSkillPickerOpen(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 999,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
lineHeight: 1,
|
||||
color: '#111111',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)'
|
||||
}}
|
||||
aria-label="关闭技能管理"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', paddingRight: 56 }}>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 800, color: '#111111' }}>技能管理</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>
|
||||
为 {selectedAgent.name} 添加共享技能,或创建本地技能
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>创建本地技能</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input
|
||||
value={newLocalSkillName}
|
||||
onChange={(e) => setNewLocalSkillName(e.target.value)}
|
||||
placeholder="输入技能名,例如 event_playbook"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: 11,
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onCreateLocalSkill) {
|
||||
onCreateLocalSkill(newLocalSkillName);
|
||||
setNewLocalSkillName('');
|
||||
}
|
||||
}}
|
||||
disabled={!isConnected || !newLocalSkillName.trim()}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && newLocalSkillName.trim() ? '#EFF6FF' : '#E5E7EB',
|
||||
color: '#1565C0',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && newLocalSkillName.trim() ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>添加共享技能</div>
|
||||
<div style={{
|
||||
border: '1px solid #E5E7EB',
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 8,
|
||||
padding: '10px 12px',
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
maxHeight: 360,
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{availableSkills.length === 0 ? (
|
||||
<div style={{ fontSize: 11, color: '#6B7280' }}>没有可添加的共享技能</div>
|
||||
) : availableSkills.map((skill) => {
|
||||
const saving = agentSkillsSavingKey === `${selectedAgentId}:${skill.skill_name}`;
|
||||
return (
|
||||
<div
|
||||
key={skill.skill_name}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
alignItems: 'flex-start',
|
||||
paddingBottom: 10,
|
||||
borderBottom: '1px dashed #D7DEE7'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>
|
||||
{skill.name || skill.skill_name}
|
||||
</span>
|
||||
<span style={{
|
||||
padding: '2px 6px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid #D0D7DE',
|
||||
color: '#6B7280',
|
||||
fontSize: 9,
|
||||
fontWeight: 700
|
||||
}}>
|
||||
共享
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#4B5563' }}>
|
||||
{skill.description || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSkillToggle(skill.skill_name, true)}
|
||||
disabled={!isConnected || saving}
|
||||
style={{
|
||||
padding: '7px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #1565C0',
|
||||
background: isConnected && !saving ? '#EFF6FF' : '#E5E7EB',
|
||||
color: '#1565C0',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
cursor: isConnected && !saving ? 'pointer' : 'not-allowed',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '处理中' : '添加'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user