feat: Add agent workspace system and runtime management
- Add agent core modules (agent_core, factory, registry, skill_loader) - Add runtime system for agent execution management - Add REST API for agents, workspaces, and runtime control - Add process supervisor for agent lifecycle management - Add workspace template system with agent profiles - Add frontend RuntimeView and runtime API integration - Add per-agent skill workspaces for smoke_fullstack run - Refactor skill system with active/installed separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ function getRankMedal(rank) {
|
||||
* Supports click and hover (1.5s) to show agent performance cards
|
||||
* Supports replay mode - completely independent from live mode
|
||||
*/
|
||||
export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJumpToMessage }) {
|
||||
export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJumpToMessage, onOpenLaunchConfig }) {
|
||||
const canvasRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@@ -719,13 +719,24 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Replay Button */}
|
||||
{showReplayButton && (
|
||||
{/* Room Controls */}
|
||||
{(showReplayButton || onOpenLaunchConfig) && (
|
||||
<div className="replay-button-container">
|
||||
{onOpenLaunchConfig && (
|
||||
<button
|
||||
className="replay-button"
|
||||
onClick={onOpenLaunchConfig}
|
||||
title="打开启动配置"
|
||||
style={{ background: '#FFFFFF', color: '#000000' }}
|
||||
>
|
||||
<span>启动</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="replay-button"
|
||||
onClick={handleReplayClick}
|
||||
title="Replay feed history"
|
||||
disabled={!showReplayButton}
|
||||
>
|
||||
<span className="replay-icon">▶▶</span>
|
||||
<span>回放</span>
|
||||
@@ -767,4 +778,3 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, feed, onJump
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,247 +1,471 @@
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export default function RuntimeSettingsPanel({
|
||||
showTrigger = true,
|
||||
isOpen,
|
||||
isConnected,
|
||||
isSaving,
|
||||
feedback,
|
||||
runtimeConfig,
|
||||
scheduleMode,
|
||||
intervalMinutes,
|
||||
triggerTime,
|
||||
maxCommCycles,
|
||||
initialCash,
|
||||
marginRequirement,
|
||||
enableMemory,
|
||||
watchlistSymbols,
|
||||
watchlistInputValue,
|
||||
watchlistSuggestions,
|
||||
onToggle,
|
||||
onClose,
|
||||
onScheduleModeChange,
|
||||
onIntervalMinutesChange,
|
||||
onTriggerTimeChange,
|
||||
onMaxCommCyclesChange,
|
||||
onInitialCashChange,
|
||||
onMarginRequirementChange,
|
||||
onEnableMemoryChange,
|
||||
onWatchlistInputChange,
|
||||
onWatchlistInputKeyDown,
|
||||
onWatchlistAdd,
|
||||
onWatchlistRemove,
|
||||
onWatchlistRestoreCurrent,
|
||||
onWatchlistRestoreDefault,
|
||||
onWatchlistSuggestionClick,
|
||||
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>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
||||
{showTrigger && (
|
||||
<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>
|
||||
{isOpen && createPortal((
|
||||
<div
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(15, 23, 42, 0.28)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
zIndex: 9998
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(event) => event.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
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: '#666666',
|
||||
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',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1
|
||||
boxShadow: '0 4px 12px rgba(15, 23, 42, 0.08)'
|
||||
}}
|
||||
aria-label="关闭启动配置"
|
||||
>
|
||||
×
|
||||
</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 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' }}>
|
||||
配置本次任务的启动参数与调度方式
|
||||
</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 style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 12
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>自选股</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
minHeight: 40,
|
||||
padding: '2px 0'
|
||||
}}>
|
||||
{watchlistSymbols.map((symbol) => (
|
||||
<button
|
||||
key={symbol}
|
||||
onClick={() => onWatchlistRemove(symbol)}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<span>{symbol}</span>
|
||||
<span style={{ color: '#777777' }}>×</span>
|
||||
</button>
|
||||
))}
|
||||
{watchlistSymbols.length === 0 && (
|
||||
<div style={{ fontSize: '11px', color: '#888888', padding: '8px 2px' }}>
|
||||
还没有股票,输入代码后回车添加
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
value={watchlistInputValue}
|
||||
onChange={(e) => onWatchlistInputChange(e.target.value)}
|
||||
onKeyDown={onWatchlistInputKeyDown}
|
||||
placeholder="输入股票代码,回车添加"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '9px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={onWatchlistAdd}
|
||||
style={{
|
||||
padding: '9px 12px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{watchlistSuggestions.map((symbol) => {
|
||||
const active = watchlistSymbols.includes(symbol);
|
||||
return (
|
||||
<button
|
||||
key={symbol}
|
||||
onClick={() => onWatchlistSuggestionClick(symbol)}
|
||||
disabled={active}
|
||||
style={{
|
||||
padding: '5px 8px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid',
|
||||
borderColor: active ? '#B6E3C5' : '#D0D7DE',
|
||||
background: active ? '#ECFDF3' : '#FFFFFF',
|
||||
color: active ? '#157347' : '#4A5568',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
cursor: active ? 'default' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{symbol}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onWatchlistRestoreCurrent}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复当前
|
||||
</button>
|
||||
<button
|
||||
onClick={onWatchlistRestoreDefault}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 12
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>调度参数</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: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value="daily">每日定时</option>
|
||||
<option value="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: 8,
|
||||
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 }}>每日定时时间 (NYSE)</span>
|
||||
<input
|
||||
type="time"
|
||||
value={triggerTime}
|
||||
onChange={(e) => onTriggerTimeChange(e.target.value)}
|
||||
disabled={scheduleMode !== 'daily'}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 8,
|
||||
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: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
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"
|
||||
step="1000"
|
||||
value={initialCash}
|
||||
onChange={(e) => onInitialCashChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
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="0"
|
||||
step="0.01"
|
||||
value={marginRequirement}
|
||||
onChange={(e) => onMarginRequirementChange(e.target.value)}
|
||||
style={{
|
||||
padding: '9px 10px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 2 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableMemory}
|
||||
onChange={(e) => onEnableMemoryChange(e.target.checked)}
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
accentColor: '#0D47A1',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '11px', color: '#111111', fontWeight: 700 }}>启用长期记忆</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
border: '1px solid #E5EAF1',
|
||||
borderRadius: 12,
|
||||
background: '#FCFDFE',
|
||||
padding: 14,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: '#111111' }}>操作</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onRestoreDefaults}
|
||||
style={{
|
||||
padding: '9px 12px',
|
||||
borderRadius: 8,
|
||||
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: 8,
|
||||
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>
|
||||
</div>
|
||||
|
||||
{feedback && (
|
||||
<span style={{
|
||||
color: feedback.type === 'success' ? '#00C853' : '#FF5252',
|
||||
fontSize: '11px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{feedback.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
), document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
556
frontend/src/components/RuntimeView.jsx
Normal file
556
frontend/src/components/RuntimeView.jsx
Normal file
@@ -0,0 +1,556 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
approvePendingApproval,
|
||||
denyPendingApproval,
|
||||
loadAllRuntimeState
|
||||
} from '../services/runtimeApi';
|
||||
|
||||
const AUTO_REFRESH_MS = 5000;
|
||||
|
||||
const STATUS_LABELS = {
|
||||
idle: '空闲',
|
||||
registered: '已注册',
|
||||
initializing: '初始化中',
|
||||
ready: '就绪',
|
||||
running: '运行中',
|
||||
analysis_in_progress: '分析中',
|
||||
risk_review_in_progress: '风控处理中',
|
||||
discussion_in_progress: '会商中',
|
||||
decision_in_progress: '决策中',
|
||||
execution_in_progress: '执行中',
|
||||
settlement_in_progress: '结算中',
|
||||
reflection_in_progress: '复盘中',
|
||||
waiting_approval: '等待审批',
|
||||
approved: '已批准',
|
||||
denied: '已拒绝',
|
||||
completed: '已完成',
|
||||
error: '异常',
|
||||
stopped: '已停止'
|
||||
};
|
||||
|
||||
const EVENT_FILTER_OPTIONS = [
|
||||
{ value: 'all', label: '全部事件' },
|
||||
{ value: 'cycle', label: '运行周期' },
|
||||
{ value: 'approval', label: '审批事件' }
|
||||
];
|
||||
|
||||
function metricCard(label, value, accent, helper = null) {
|
||||
return (
|
||||
<div className="stat-card">
|
||||
<div className="stat-card-label">
|
||||
{label}
|
||||
</div>
|
||||
<div className="stat-card-value" style={{ color: accent }}>
|
||||
{value}
|
||||
</div>
|
||||
{helper && (
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: '#666666', lineHeight: 1.5 }}>
|
||||
{helper}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function resolveApprovalTone(approval) {
|
||||
const findings = Array.isArray(approval.findings) ? approval.findings : [];
|
||||
const levels = findings.map((item) => item?.severity).filter(Boolean);
|
||||
if (levels.includes('critical')) {
|
||||
return { border: '#7F1D1D', bg: '#FEF2F2', text: '#991B1B', badgeBg: '#FECACA' };
|
||||
}
|
||||
if (levels.includes('high')) {
|
||||
return { border: '#9A3412', bg: '#FFF7ED', text: '#C2410C', badgeBg: '#FED7AA' };
|
||||
}
|
||||
if (levels.includes('medium')) {
|
||||
return { border: '#92400E', bg: '#FFFBEB', text: '#B45309', badgeBg: '#FDE68A' };
|
||||
}
|
||||
return { border: '#D1D5DB', bg: '#FCFCFC', text: '#374151', badgeBg: '#E5E7EB' };
|
||||
}
|
||||
|
||||
function sectionTitle(label, action = null) {
|
||||
return (
|
||||
<div className="section-header" style={{ marginBottom: 0 }}>
|
||||
<div className="section-title" style={{ fontSize: 14 }}>
|
||||
{label}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatStatusLabel(status) {
|
||||
if (!status) {
|
||||
return '-';
|
||||
}
|
||||
return STATUS_LABELS[status] || status.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function formatSessionLabel(sessionId) {
|
||||
return sessionId || '无会话';
|
||||
}
|
||||
|
||||
function formatEventLabel(eventName) {
|
||||
if (!eventName) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const [group, action] = String(eventName).split(':');
|
||||
if (group === 'cycle') {
|
||||
if (action === 'start') return '周期开始';
|
||||
if (action === 'complete') return '周期完成';
|
||||
if (action === 'error') return '周期异常';
|
||||
return '运行周期';
|
||||
}
|
||||
if (group === 'approval') {
|
||||
if (action === 'created') return '创建审批';
|
||||
if (action === 'approved') return '审批通过';
|
||||
if (action === 'denied') return '审批拒绝';
|
||||
if (action === 'expired') return '审批超时';
|
||||
return '审批事件';
|
||||
}
|
||||
if (group === 'agent') {
|
||||
if (action === 'status') return '状态更新';
|
||||
if (action === 'registered') return '注册 Agent';
|
||||
return 'Agent 事件';
|
||||
}
|
||||
|
||||
return String(eventName).replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
export default function RuntimeView() {
|
||||
const [runtimeState, setRuntimeState] = useState(null);
|
||||
const [runtimeError, setRuntimeError] = useState(null);
|
||||
const [isRuntimeLoading, setIsRuntimeLoading] = useState(false);
|
||||
const [approvalActionId, setApprovalActionId] = useState(null);
|
||||
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true);
|
||||
const [eventFilter, setEventFilter] = useState('all');
|
||||
|
||||
const refreshRuntimeState = () => {
|
||||
setIsRuntimeLoading(true);
|
||||
loadAllRuntimeState(
|
||||
(state) => {
|
||||
setRuntimeState(state);
|
||||
setRuntimeError(null);
|
||||
setIsRuntimeLoading(false);
|
||||
},
|
||||
(error) => {
|
||||
setRuntimeError(error.message || '无法加载运行状态');
|
||||
setIsRuntimeLoading(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshRuntimeState();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefreshEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
refreshRuntimeState();
|
||||
}, AUTO_REFRESH_MS);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [autoRefreshEnabled]);
|
||||
|
||||
const handleApprovalAction = async (approvalId, action) => {
|
||||
setApprovalActionId(approvalId);
|
||||
try {
|
||||
if (action === 'approve') {
|
||||
await approvePendingApproval(approvalId);
|
||||
} else {
|
||||
await denyPendingApproval(approvalId);
|
||||
}
|
||||
refreshRuntimeState();
|
||||
} catch (error) {
|
||||
setRuntimeError(error.message || '审批操作失败');
|
||||
setIsRuntimeLoading(false);
|
||||
} finally {
|
||||
setApprovalActionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const agents = runtimeState?.agents || [];
|
||||
const approvals = runtimeState?.approvals || [];
|
||||
const events = runtimeState?.events || [];
|
||||
const activeAgentsCount = agents.filter((agent) => agent.status && agent.status !== 'idle').length;
|
||||
const visibleEvents = events
|
||||
.filter((event) => eventFilter === 'all' || event.event.startsWith(eventFilter))
|
||||
.slice()
|
||||
.reverse();
|
||||
|
||||
return (
|
||||
<div className="performance-page" style={{ height: '100%', minHeight: 0 }}>
|
||||
<div className="section">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<div className="section-title" style={{ fontSize: 18 }}>
|
||||
运行态控制台
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
color: '#666666',
|
||||
marginTop: 4,
|
||||
maxWidth: 760,
|
||||
lineHeight: 1.5
|
||||
}}>
|
||||
查看当前运行上下文、分析师状态、待审批请求与近期事件。这里是监控面板,不再和运行设置挤在同一个小弹层里。
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={refreshRuntimeState}
|
||||
disabled={isRuntimeLoading}
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #111111',
|
||||
background: isRuntimeLoading ? '#8A8A8A' : '#111111',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.4px',
|
||||
cursor: isRuntimeLoading ? 'not-allowed' : 'pointer',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{isRuntimeLoading ? '刷新中' : '刷新运行态'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
|
||||
{metricCard('活跃 Agent', activeAgentsCount, '#2563EB', `共 ${agents.length} 个 agent 已注册`)}
|
||||
{metricCard('待审批', approvals.length, approvals.length > 0 ? '#C2410C' : '#059669', approvals.length > 0 ? '需要人工处理' : '当前无待处理审批')}
|
||||
{metricCard('运行事件', events.length, '#111111', '最近运行阶段和状态变化')}
|
||||
<div className="stat-card">
|
||||
<div className="stat-card-label">
|
||||
自动刷新
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAutoRefreshEnabled((value) => !value)}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
border: '1px solid #000000',
|
||||
background: autoRefreshEnabled ? '#000000' : '#FFFFFF',
|
||||
color: autoRefreshEnabled ? '#FFFFFF' : '#000000',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
textTransform: 'uppercase',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{autoRefreshEnabled ? `开启 / ${AUTO_REFRESH_MS / 1000}秒` : '关闭'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{runtimeError && (
|
||||
<div className="section" style={{
|
||||
borderColor: '#FF1744',
|
||||
background: '#FFF5F7',
|
||||
color: '#B91C1C',
|
||||
fontSize: 12,
|
||||
fontWeight: 700
|
||||
}}>
|
||||
{runtimeError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: 20,
|
||||
alignContent: 'start'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(320px, 0.95fr) minmax(360px, 1.25fr)',
|
||||
gap: 20,
|
||||
alignItems: 'start'
|
||||
}}>
|
||||
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
|
||||
{sectionTitle('运行上下文')}
|
||||
{runtimeState?.context ? (
|
||||
<div style={{
|
||||
border: '1px solid #000000',
|
||||
background: '#FAFAFA',
|
||||
padding: 12,
|
||||
display: 'grid',
|
||||
gap: 10
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase' }}>配置名</div>
|
||||
<div style={{ fontSize: 18, color: '#111111', fontWeight: 800, marginTop: 3 }}>
|
||||
{runtimeState.context.config_name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase' }}>运行目录</div>
|
||||
<div style={{ fontSize: 11, color: '#111111', lineHeight: 1.5, marginTop: 3, wordBreak: 'break-all' }}>
|
||||
{runtimeState.context.run_dir}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', textTransform: 'uppercase' }}>启动参数</div>
|
||||
<pre style={{
|
||||
margin: '6px 0 0',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.7,
|
||||
color: '#111111',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{JSON.stringify(runtimeState.context.bootstrap_values || {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: 12, color: '#9CA3AF' }}>暂无运行上下文</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
|
||||
{sectionTitle('待审批请求')}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
maxHeight: 640,
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4
|
||||
}}>
|
||||
{approvals.length ? approvals.map((approval) => {
|
||||
const tone = resolveApprovalTone(approval);
|
||||
return (
|
||||
<div
|
||||
key={approval.approval_id}
|
||||
style={{
|
||||
border: `1px solid ${tone.border}`,
|
||||
background: '#FFFFFF',
|
||||
padding: 12,
|
||||
display: 'grid',
|
||||
gap: 8
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 800, color: '#111111' }}>
|
||||
{approval.tool_name}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.5px',
|
||||
padding: '4px 6px',
|
||||
background: tone.badgeBg,
|
||||
color: tone.text,
|
||||
border: `1px solid ${tone.border}`,
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
{formatStatusLabel(approval.status)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6B7280', lineHeight: 1.5 }}>
|
||||
{approval.agent_id} · {approval.workspace_id} · {formatSessionLabel(approval.session_id)}
|
||||
</div>
|
||||
{approval.tool_input && (
|
||||
<pre style={{
|
||||
margin: 0,
|
||||
padding: 10,
|
||||
background: '#FAFAFA',
|
||||
border: '1px solid #000000',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.6,
|
||||
color: '#111111',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{JSON.stringify(approval.tool_input, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{approval.findings?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{approval.findings.map((finding, index) => (
|
||||
<span
|
||||
key={`${approval.approval_id}-finding-${index}`}
|
||||
style={{
|
||||
padding: '4px 6px',
|
||||
background: '#FFFFFF',
|
||||
border: `1px solid ${tone.border}`,
|
||||
color: tone.text,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
{finding.severity}: {finding.message}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<button
|
||||
onClick={() => handleApprovalAction(approval.approval_id, 'deny')}
|
||||
disabled={approvalActionId === approval.approval_id}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
border: '1px solid #000000',
|
||||
background: '#FFFFFF',
|
||||
color: '#000000',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
cursor: approvalActionId === approval.approval_id ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleApprovalAction(approval.approval_id, 'approve')}
|
||||
disabled={approvalActionId === approval.approval_id}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
border: '1px solid #000000',
|
||||
background: '#000000',
|
||||
color: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
cursor: approvalActionId === approval.approval_id ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
批准
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}) : (
|
||||
<div style={{
|
||||
border: '1px dashed #999999',
|
||||
padding: 16,
|
||||
fontSize: 12,
|
||||
color: '#666666',
|
||||
background: '#FAFAFA'
|
||||
}}>
|
||||
当前无待审批请求
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(320px, 1fr) minmax(360px, 1fr)',
|
||||
gap: 20,
|
||||
alignItems: 'start'
|
||||
}}>
|
||||
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
|
||||
{sectionTitle('Agent 状态')}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: 8,
|
||||
maxHeight: 420,
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4
|
||||
}}>
|
||||
{runtimeState?.agents?.length ? runtimeState.agents.map((agent) => (
|
||||
<div
|
||||
key={agent.agent_id}
|
||||
style={{
|
||||
border: '1px solid #000000',
|
||||
background: '#FAFAFA',
|
||||
padding: 10,
|
||||
display: 'grid',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>{agent.agent_id}</span>
|
||||
<span style={{ fontSize: 11, color: '#2563EB', fontFamily: '"Courier New", monospace' }}>{formatStatusLabel(agent.status)}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', lineHeight: 1.5 }}>
|
||||
会话: {formatSessionLabel(agent.last_session)}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280', lineHeight: 1.5 }}>
|
||||
更新时间: {agent.last_updated}
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ fontSize: 12, color: '#9CA3AF' }}>暂无 agent 状态</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section" style={{ display: 'grid', gap: 12, marginBottom: 0 }}>
|
||||
{sectionTitle(
|
||||
'近期事件',
|
||||
<select
|
||||
value={eventFilter}
|
||||
onChange={(event) => setEventFilter(event.target.value)}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
border: '1px solid #000000',
|
||||
background: '#FFFFFF',
|
||||
color: '#000000',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
{EVENT_FILTER_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gap: 8,
|
||||
maxHeight: 420,
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4
|
||||
}}>
|
||||
{visibleEvents.length ? visibleEvents.map((event, index) => (
|
||||
<div
|
||||
key={`${event.timestamp}-${event.event}-${index}`}
|
||||
style={{
|
||||
border: '1px solid #000000',
|
||||
background: '#FAFAFA',
|
||||
padding: 10,
|
||||
display: 'grid',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#111111' }}>{formatEventLabel(event.event)}</span>
|
||||
<span style={{ fontSize: 10, color: '#6B7280', fontFamily: '"Courier New", monospace' }}>{formatSessionLabel(event.session)}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#6B7280' }}>{event.timestamp}</div>
|
||||
{event.details && Object.keys(event.details).length > 0 && (
|
||||
<pre style={{
|
||||
margin: 0,
|
||||
fontSize: 10,
|
||||
lineHeight: 1.6,
|
||||
color: '#374151',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
{JSON.stringify(event.details, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)) : (
|
||||
<div style={{ fontSize: 12, color: '#9CA3AF' }}>当前筛选条件下暂无运行事件</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user