确认PokieTicker新闻库数据源
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/trading_logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -19,9 +19,9 @@ import AgentFeed from './components/AgentFeed';
|
||||
import StockLogo from './components/StockLogo';
|
||||
import StatisticsView from './components/StatisticsView';
|
||||
import PerformanceView from './components/PerformanceView';
|
||||
import AboutModal from './components/AboutModal';
|
||||
import RulesView from './components/RulesView';
|
||||
import StockExplainView from './components/StockExplainView.jsx';
|
||||
import Header from './components/Header.jsx';
|
||||
import WatchlistPanel from './components/WatchlistPanel.jsx';
|
||||
|
||||
// Utils
|
||||
import { formatNumber, formatTickerPrice } from './utils/formatters';
|
||||
@@ -39,9 +39,8 @@ export default function LiveTradingApp() {
|
||||
const [currentDate, setCurrentDate] = useState(null);
|
||||
const [progress, setProgress] = useState({ current: 0, total: 0 });
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
const [showAboutModal, setShowAboutModal] = useState(false);
|
||||
|
||||
// View toggle: 'rules' | 'room' | 'chart' | 'statistics'
|
||||
// View toggle: 'room' | 'explain' | 'chart' | 'statistics'
|
||||
const [currentView, setCurrentView] = useState('chart'); // Start with chart, then animate to room
|
||||
const [isInitialAnimating, setIsInitialAnimating] = useState(true);
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date());
|
||||
@@ -71,6 +70,11 @@ export default function LiveTradingApp() {
|
||||
// Ticker prices (now from real-time data)
|
||||
const [tickers, setTickers] = useState(INITIAL_TICKERS);
|
||||
const [rollingTickers, setRollingTickers] = useState({});
|
||||
const [priceHistoryByTicker, setPriceHistoryByTicker] = useState({});
|
||||
const [ohlcHistoryByTicker, setOhlcHistoryByTicker] = useState({});
|
||||
const [explainEventsByTicker, setExplainEventsByTicker] = useState({});
|
||||
const [selectedExplainSymbol, setSelectedExplainSymbol] = useState('');
|
||||
const [historySourceByTicker, setHistorySourceByTicker] = useState({});
|
||||
|
||||
// Room bubbles
|
||||
const [bubbles, setBubbles] = useState({});
|
||||
@@ -84,10 +88,18 @@ export default function LiveTradingApp() {
|
||||
const [marketStatus, setMarketStatus] = useState(null); // { status, status_text, ... }
|
||||
const [virtualTime, setVirtualTime] = useState(null); // Virtual time from server (for mock mode)
|
||||
const [dataSources, setDataSources] = useState(null);
|
||||
const [runtimeConfig, setRuntimeConfig] = useState(null);
|
||||
const [isWatchlistPanelOpen, setIsWatchlistPanelOpen] = useState(false);
|
||||
const [watchlistDraftSymbols, setWatchlistDraftSymbols] = useState([]);
|
||||
const [watchlistInputValue, setWatchlistInputValue] = useState('');
|
||||
const [watchlistFeedback, setWatchlistFeedback] = useState(null);
|
||||
const [isWatchlistSaving, setIsWatchlistSaving] = useState(false);
|
||||
|
||||
const clientRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const agentFeedRef = useRef(null);
|
||||
const isWatchlistSavingRef = useRef(false);
|
||||
const requestedStockHistoryRef = useRef(new Set());
|
||||
|
||||
// Track last virtual time update to calculate increment
|
||||
const lastVirtualTimeRef = useRef(null);
|
||||
@@ -96,12 +108,311 @@ export default function LiveTradingApp() {
|
||||
// Last day history for replay
|
||||
const [lastDayHistory, setLastDayHistory] = useState([]);
|
||||
|
||||
const buildTickersFromSymbols = useCallback((symbols, previousTickers = []) => {
|
||||
if (!Array.isArray(symbols) || symbols.length === 0) {
|
||||
return previousTickers;
|
||||
}
|
||||
|
||||
return symbols
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim())
|
||||
.map((symbol) => {
|
||||
const normalized = symbol.trim().toUpperCase();
|
||||
const existing = previousTickers.find((ticker) => ticker.symbol === normalized);
|
||||
return existing || {
|
||||
symbol: normalized,
|
||||
price: null,
|
||||
change: null
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const normalizePriceHistory = useCallback((payload) => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
Object.entries(payload).forEach(([symbol, points]) => {
|
||||
const ticker = String(symbol || '').trim().toUpperCase();
|
||||
if (!ticker || !Array.isArray(points)) {
|
||||
return;
|
||||
}
|
||||
|
||||
normalized[ticker] = points
|
||||
.map((point) => {
|
||||
if (Array.isArray(point) && point.length >= 2) {
|
||||
const [label, value] = point;
|
||||
const price = Number(value);
|
||||
if (!label || !Number.isFinite(price)) return null;
|
||||
return {
|
||||
timestamp: String(label),
|
||||
label: String(label),
|
||||
price
|
||||
};
|
||||
}
|
||||
|
||||
if (point && typeof point === 'object') {
|
||||
const rawTimestamp = point.timestamp ?? point.t ?? point.date ?? point.label;
|
||||
const price = Number(point.price ?? point.v ?? point.value ?? point.close);
|
||||
if (!rawTimestamp || !Number.isFinite(price)) return null;
|
||||
return {
|
||||
timestamp: String(rawTimestamp),
|
||||
label: String(rawTimestamp),
|
||||
price
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(-120);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}, []);
|
||||
|
||||
// Determine if LIVE tab should be enabled
|
||||
const isLiveEnabled = useMemo(() => {
|
||||
if (!marketStatus) return false;
|
||||
return marketStatus.status === 'open';
|
||||
}, [marketStatus]);
|
||||
|
||||
const displayTickers = useMemo(() => {
|
||||
const symbols = runtimeConfig?.tickers;
|
||||
if (Array.isArray(symbols) && symbols.length > 0) {
|
||||
return buildTickersFromSymbols(symbols, tickers);
|
||||
}
|
||||
return tickers;
|
||||
}, [buildTickersFromSymbols, runtimeConfig, tickers]);
|
||||
|
||||
const runtimeWatchlistSymbols = useMemo(() => {
|
||||
const symbols = runtimeConfig?.tickers;
|
||||
if (Array.isArray(symbols) && symbols.length > 0) {
|
||||
return symbols
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim())
|
||||
.map((symbol) => symbol.trim().toUpperCase());
|
||||
}
|
||||
|
||||
return displayTickers
|
||||
.map((ticker) => ticker.symbol)
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim());
|
||||
}, [displayTickers, runtimeConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const symbols = displayTickers
|
||||
.map((ticker) => ticker.symbol)
|
||||
.filter((symbol) => typeof symbol === 'string' && symbol.trim());
|
||||
|
||||
if (!symbols.length) {
|
||||
setSelectedExplainSymbol('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedExplainSymbol || !symbols.includes(selectedExplainSymbol)) {
|
||||
setSelectedExplainSymbol(symbols[0]);
|
||||
}
|
||||
}, [displayTickers, selectedExplainSymbol]);
|
||||
|
||||
const watchlistSuggestions = useMemo(
|
||||
() => INITIAL_TICKERS.map((ticker) => ticker.symbol).filter((symbol, index, list) => list.indexOf(symbol) === index),
|
||||
[]
|
||||
);
|
||||
|
||||
const isWatchlistDraftDirty = useMemo(() => {
|
||||
if (watchlistInputValue.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (watchlistDraftSymbols.length !== runtimeWatchlistSymbols.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return watchlistDraftSymbols.some((symbol, index) => symbol !== runtimeWatchlistSymbols[index]);
|
||||
}, [runtimeWatchlistSymbols, watchlistDraftSymbols, watchlistInputValue]);
|
||||
|
||||
const marketStatusLabel = useMemo(() => {
|
||||
if (!marketStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = typeof marketStatus.status_text === 'string' ? marketStatus.status_text.trim() : '';
|
||||
const normalized = raw.toLowerCase();
|
||||
if (normalized === 'market closed (non-trading day)') {
|
||||
return '休市';
|
||||
}
|
||||
if (normalized === 'market open') {
|
||||
return '开盘';
|
||||
}
|
||||
if (normalized === 'market closed') {
|
||||
return '收盘';
|
||||
}
|
||||
|
||||
return raw || (marketStatus.status === 'open' ? '开盘' : '收盘');
|
||||
}, [marketStatus]);
|
||||
|
||||
const priceSourceLabel = useMemo(() => {
|
||||
const source = dataSources?.last_success?.prices;
|
||||
if (!source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = String(source).trim().toLowerCase();
|
||||
const labels = {
|
||||
yfinance: '数据源 Yahoo',
|
||||
finnhub: '数据源 Finnhub',
|
||||
financial_datasets: '数据源 Financial Datasets',
|
||||
local_csv: '数据源 CSV'
|
||||
};
|
||||
|
||||
return labels[normalized] || `数据源 ${String(source).trim()}`;
|
||||
}, [dataSources]);
|
||||
|
||||
const parseWatchlistInput = useCallback((value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
value
|
||||
.split(/[\s,]+/)
|
||||
.map((symbol) => symbol.trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const commitWatchlistInput = useCallback((value) => {
|
||||
const parsed = parseWatchlistInput(value);
|
||||
if (parsed.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
setWatchlistDraftSymbols((prev) => Array.from(new Set([...prev, ...parsed])));
|
||||
setWatchlistInputValue('');
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
return parsed;
|
||||
}, [parseWatchlistInput, watchlistFeedback]);
|
||||
|
||||
const handleWatchlistRemove = useCallback((symbolToRemove) => {
|
||||
setWatchlistDraftSymbols((prev) => prev.filter((symbol) => symbol !== symbolToRemove));
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistFeedback]);
|
||||
|
||||
const handleWatchlistPanelToggle = useCallback(() => {
|
||||
setIsWatchlistPanelOpen((open) => {
|
||||
const nextOpen = !open;
|
||||
if (nextOpen) {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
return nextOpen;
|
||||
});
|
||||
}, [runtimeWatchlistSymbols]);
|
||||
|
||||
const handleWatchlistInputChange = useCallback((value) => {
|
||||
setWatchlistInputValue(value);
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistFeedback]);
|
||||
|
||||
const handleWatchlistInputKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
commitWatchlistInput(watchlistInputValue);
|
||||
}
|
||||
}, [commitWatchlistInput, watchlistInputValue]);
|
||||
|
||||
const handleWatchlistSuggestionClick = useCallback((symbol) => {
|
||||
if (watchlistDraftSymbols.includes(symbol)) {
|
||||
return;
|
||||
}
|
||||
setWatchlistDraftSymbols((prev) => [...prev, symbol]);
|
||||
if (watchlistFeedback) {
|
||||
setWatchlistFeedback(null);
|
||||
}
|
||||
}, [watchlistDraftSymbols, watchlistFeedback]);
|
||||
|
||||
const handleWatchlistRestoreCurrent = useCallback(() => {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}, [runtimeWatchlistSymbols]);
|
||||
|
||||
const handleWatchlistRestoreDefault = useCallback(() => {
|
||||
setWatchlistDraftSymbols(watchlistSuggestions);
|
||||
setWatchlistInputValue('');
|
||||
setWatchlistFeedback(null);
|
||||
}, [watchlistSuggestions]);
|
||||
|
||||
const handleWatchlistSave = useCallback(() => {
|
||||
const pendingTickers = parseWatchlistInput(watchlistInputValue);
|
||||
const nextTickers = Array.from(new Set([...watchlistDraftSymbols, ...pendingTickers]));
|
||||
if (nextTickers.length === 0) {
|
||||
setWatchlistFeedback({ type: 'error', text: '至少输入 1 个有效股票代码' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientRef.current) {
|
||||
setWatchlistFeedback({ type: 'error', text: '连接未就绪,稍后重试' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsWatchlistSaving(true);
|
||||
setWatchlistFeedback(null);
|
||||
setWatchlistDraftSymbols(nextTickers);
|
||||
setWatchlistInputValue('');
|
||||
const success = clientRef.current.send({
|
||||
type: 'update_watchlist',
|
||||
tickers: nextTickers
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({ type: 'error', text: '发送失败,请检查连接状态' });
|
||||
}
|
||||
}, [parseWatchlistInput, watchlistDraftSymbols, watchlistInputValue]);
|
||||
|
||||
const requestStockHistory = useCallback((symbol, { force = false } = {}) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!force && requestedStockHistoryRef.current.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = clientRef.current.send({
|
||||
type: 'get_stock_history',
|
||||
ticker: normalized,
|
||||
lookback_days: 120
|
||||
});
|
||||
|
||||
if (success) {
|
||||
requestedStockHistoryRef.current.add(normalized);
|
||||
}
|
||||
|
||||
return success;
|
||||
}, []);
|
||||
|
||||
const requestStockExplainEvents = useCallback((symbol) => {
|
||||
const normalized = typeof symbol === 'string' ? symbol.trim().toUpperCase() : '';
|
||||
if (!normalized || !clientRef.current) {
|
||||
return false;
|
||||
}
|
||||
return clientRef.current.send({
|
||||
type: 'get_stock_explain_events',
|
||||
ticker: normalized
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Switch away from LIVE tab when market closes
|
||||
useEffect(() => {
|
||||
if (!isLiveEnabled && chartTab === 'live') {
|
||||
@@ -109,6 +420,27 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
}, [isLiveEnabled, chartTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWatchlistPanelOpen || !isWatchlistDraftDirty) {
|
||||
setWatchlistDraftSymbols(runtimeWatchlistSymbols);
|
||||
if (!isWatchlistPanelOpen) {
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
}
|
||||
}, [isWatchlistDraftDirty, isWatchlistPanelOpen, runtimeWatchlistSymbols]);
|
||||
|
||||
useEffect(() => {
|
||||
isWatchlistSavingRef.current = isWatchlistSaving;
|
||||
}, [isWatchlistSaving]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView !== 'explain' || !selectedExplainSymbol) {
|
||||
return;
|
||||
}
|
||||
requestStockHistory(selectedExplainSymbol);
|
||||
requestStockExplainEvents(selectedExplainSymbol);
|
||||
}, [currentView, requestStockExplainEvents, requestStockHistory, selectedExplainSymbol]);
|
||||
|
||||
// Clock - use virtual time if available (for mock mode)
|
||||
useEffect(() => {
|
||||
if (virtualTime) {
|
||||
@@ -253,6 +585,10 @@ export default function LiveTradingApp() {
|
||||
// Error response (for fast forward errors)
|
||||
error: (e) => {
|
||||
console.error('[Error]', e.message);
|
||||
if (isWatchlistSavingRef.current) {
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({ type: 'error', text: e.message || '更新 watchlist 失败' });
|
||||
}
|
||||
|
||||
// Handle fast forward errors
|
||||
if (e.message && e.message.includes('fast forward')) {
|
||||
@@ -307,6 +643,12 @@ export default function LiveTradingApp() {
|
||||
if (state.data_sources) {
|
||||
setDataSources(state.data_sources);
|
||||
}
|
||||
if (state.runtime_config) {
|
||||
setRuntimeConfig(state.runtime_config);
|
||||
}
|
||||
if (Array.isArray(state.tickers) && state.tickers.length > 0) {
|
||||
setTickers(prevTickers => buildTickersFromSymbols(state.tickers, prevTickers));
|
||||
}
|
||||
// 检查是否是mock模式
|
||||
const isMockMode = state.is_mock_mode === true;
|
||||
if (state.market_status) {
|
||||
@@ -356,6 +698,9 @@ export default function LiveTradingApp() {
|
||||
if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard);
|
||||
}
|
||||
if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices);
|
||||
if (state.price_history) {
|
||||
setPriceHistoryByTicker(normalizePriceHistory(state.price_history));
|
||||
}
|
||||
|
||||
// Load and process historical feed data
|
||||
if (state.feed_history && Array.isArray(state.feed_history)) {
|
||||
@@ -388,6 +733,75 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
},
|
||||
|
||||
runtime_assets_reloaded: (e) => {
|
||||
if (e.runtime_config_applied) {
|
||||
setRuntimeConfig(e.runtime_config_applied);
|
||||
}
|
||||
if (Array.isArray(e.runtime_config_applied?.tickers)) {
|
||||
setTickers(prevTickers => buildTickersFromSymbols(e.runtime_config_applied.tickers, prevTickers));
|
||||
setWatchlistDraftSymbols(e.runtime_config_applied.tickers.map((symbol) => String(symbol).trim().toUpperCase()));
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
if (isWatchlistSavingRef.current) {
|
||||
setIsWatchlistSaving(false);
|
||||
}
|
||||
addSystemMessage('运行时配置已热更新');
|
||||
},
|
||||
|
||||
watchlist_updated: (e) => {
|
||||
if (Array.isArray(e.tickers)) {
|
||||
const normalizedTickers = e.tickers.map((symbol) => String(symbol).trim().toUpperCase());
|
||||
requestedStockHistoryRef.current = new Set(
|
||||
Array.from(requestedStockHistoryRef.current).filter((symbol) => normalizedTickers.includes(symbol))
|
||||
);
|
||||
setRuntimeConfig((prev) => ({
|
||||
...(prev || {}),
|
||||
tickers: normalizedTickers
|
||||
}));
|
||||
setTickers((prevTickers) => buildTickersFromSymbols(normalizedTickers, prevTickers));
|
||||
setWatchlistDraftSymbols(normalizedTickers);
|
||||
setWatchlistInputValue('');
|
||||
}
|
||||
setIsWatchlistSaving(false);
|
||||
setWatchlistFeedback({
|
||||
type: 'success',
|
||||
text: `已更新为 ${Array.isArray(e.tickers) ? e.tickers.join(', ') : '最新列表'}`
|
||||
});
|
||||
},
|
||||
|
||||
stock_history_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(e.prices)) {
|
||||
setOhlcHistoryByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: e.prices
|
||||
}));
|
||||
setHistorySourceByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: e.source || null
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
stock_explain_events_loaded: (e) => {
|
||||
const symbol = typeof e.ticker === 'string' ? e.ticker.trim().toUpperCase() : '';
|
||||
if (!symbol) {
|
||||
return;
|
||||
}
|
||||
setExplainEventsByTicker((prev) => ({
|
||||
...prev,
|
||||
[symbol]: {
|
||||
events: Array.isArray(e.events) ? e.events : [],
|
||||
signals: Array.isArray(e.signals) ? e.signals : [],
|
||||
trades: Array.isArray(e.trades) ? e.trades : []
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
// Real-time price updates
|
||||
price_update: (e) => {
|
||||
try {
|
||||
@@ -402,6 +816,24 @@ export default function LiveTradingApp() {
|
||||
setIsConnected(true);
|
||||
console.log(`[Price Update] ${symbol}: $${price} (ret: ${ret !== undefined ? ret.toFixed(2) : 'N/A'}%)`);
|
||||
|
||||
setPriceHistoryByTicker((prev) => {
|
||||
const ticker = String(symbol).trim().toUpperCase();
|
||||
const nextPoint = {
|
||||
timestamp: new Date().toISOString(),
|
||||
label: now.toISOString(),
|
||||
price: Number(price)
|
||||
};
|
||||
const existing = Array.isArray(prev[ticker]) ? prev[ticker] : [];
|
||||
const lastPoint = existing[existing.length - 1];
|
||||
if (lastPoint && Number(lastPoint.price) === Number(nextPoint.price)) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[ticker]: [...existing, nextPoint].slice(-120)
|
||||
};
|
||||
});
|
||||
|
||||
// Update ticker price with animation
|
||||
setTickers(prevTickers => {
|
||||
return prevTickers.map(ticker => {
|
||||
@@ -714,7 +1146,7 @@ export default function LiveTradingApp() {
|
||||
clientRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, []); // Empty dependency array - only run once on mount
|
||||
}, [addSystemMessage, buildTickersFromSymbols, processFeedEvent, processHistoricalFeed]); // Only reconnect if handlers change
|
||||
|
||||
// Resizing handlers
|
||||
const handleMouseDown = (e) => {
|
||||
@@ -755,10 +1187,7 @@ export default function LiveTradingApp() {
|
||||
|
||||
{/* Header */}
|
||||
<div className="header">
|
||||
<Header
|
||||
onEvoTradersClick={() => setShowAboutModal(true)}
|
||||
evoTradersLinkStyle="default"
|
||||
/>
|
||||
<Header />
|
||||
|
||||
<div className="header-right" style={{ display: 'flex', alignItems: 'center', gap: 24, marginLeft: 'auto', flexWrap: 'wrap', minWidth: 0 }}>
|
||||
{/* Mock Mode Indicator */}
|
||||
@@ -885,21 +1314,41 @@ export default function LiveTradingApp() {
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className={`market-text ${serverMode === 'backtest' ? 'backtest' : (marketStatus.status === 'open' ? 'open' : 'closed')}`}>
|
||||
{marketStatus.status_text || (marketStatus.status === 'open' ? '开盘' : '收盘')}
|
||||
{marketStatusLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{dataSources?.last_success?.prices && (
|
||||
{priceSourceLabel && (
|
||||
<>
|
||||
<span className="status-sep">·</span>
|
||||
<span className="market-text backtest">
|
||||
DATA {String(dataSources.last_success.prices).toUpperCase()}
|
||||
{priceSourceLabel}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="status-sep">·</span>
|
||||
<span className="time-text">{lastUpdate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })}</span>
|
||||
</div>
|
||||
|
||||
<WatchlistPanel
|
||||
isOpen={isWatchlistPanelOpen}
|
||||
isConnected={isConnected}
|
||||
isSaving={isWatchlistSaving}
|
||||
draftSymbols={watchlistDraftSymbols}
|
||||
inputValue={watchlistInputValue}
|
||||
feedback={watchlistFeedback}
|
||||
suggestions={watchlistSuggestions}
|
||||
onToggle={handleWatchlistPanelToggle}
|
||||
onClose={() => setIsWatchlistPanelOpen(false)}
|
||||
onInputChange={handleWatchlistInputChange}
|
||||
onInputKeyDown={handleWatchlistInputKeyDown}
|
||||
onAdd={() => commitWatchlistInput(watchlistInputValue)}
|
||||
onRemove={handleWatchlistRemove}
|
||||
onRestoreCurrent={handleWatchlistRestoreCurrent}
|
||||
onRestoreDefault={handleWatchlistRestoreDefault}
|
||||
onSuggestionClick={handleWatchlistSuggestionClick}
|
||||
onSave={handleWatchlistSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -910,7 +1359,7 @@ export default function LiveTradingApp() {
|
||||
<div className="ticker-track">
|
||||
{[0, 1].map((groupIdx) => (
|
||||
<div key={groupIdx} className="ticker-group">
|
||||
{tickers.map(ticker => (
|
||||
{displayTickers.map(ticker => (
|
||||
<div key={`${ticker.symbol}-${groupIdx}`} className="ticker-item">
|
||||
<StockLogo ticker={ticker.symbol} size={16} />
|
||||
<span className="ticker-symbol">{ticker.symbol}</span>
|
||||
@@ -947,13 +1396,6 @@ export default function LiveTradingApp() {
|
||||
<div className="chart-section">
|
||||
<div className="view-container">
|
||||
<div className="view-nav-bar">
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'rules' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('rules')}
|
||||
>
|
||||
规则
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'room' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('room')}
|
||||
@@ -961,6 +1403,13 @@ export default function LiveTradingApp() {
|
||||
交易室
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'explain' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('explain')}
|
||||
>
|
||||
个股解释
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`view-nav-btn ${currentView === 'chart' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentView('chart')}
|
||||
@@ -977,12 +1426,15 @@ export default function LiveTradingApp() {
|
||||
</div>
|
||||
|
||||
{/* Slider container with four views */}
|
||||
<div className={`view-slider-four ${currentView === 'rules' ? 'show-rules' : currentView === 'room' ? 'show-room' : currentView === 'statistics' ? 'show-statistics' : 'show-chart'} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Rules View Panel */}
|
||||
<div className="view-panel">
|
||||
<RulesView />
|
||||
</div>
|
||||
|
||||
<div className={`view-slider-four ${
|
||||
currentView === 'room'
|
||||
? 'show-room'
|
||||
: currentView === 'explain'
|
||||
? 'show-explain'
|
||||
: currentView === 'statistics'
|
||||
? 'show-statistics'
|
||||
: 'show-chart'
|
||||
} ${!isInitialAnimating ? 'normal-speed' : ''}`}>
|
||||
{/* Room View Panel */}
|
||||
<div className="view-panel">
|
||||
<RoomView
|
||||
@@ -994,6 +1446,23 @@ export default function LiveTradingApp() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stock Explain View Panel */}
|
||||
<div className="view-panel">
|
||||
<StockExplainView
|
||||
tickers={displayTickers}
|
||||
holdings={holdings}
|
||||
trades={trades}
|
||||
leaderboard={leaderboard}
|
||||
feed={feed}
|
||||
priceHistoryByTicker={priceHistoryByTicker}
|
||||
ohlcHistoryByTicker={ohlcHistoryByTicker}
|
||||
selectedSymbol={selectedExplainSymbol}
|
||||
onSelectedSymbolChange={setSelectedExplainSymbol}
|
||||
selectedHistorySource={historySourceByTicker[selectedExplainSymbol] || null}
|
||||
explainEventsSnapshot={explainEventsByTicker[selectedExplainSymbol] || null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart View Panel */}
|
||||
<div className="view-panel">
|
||||
<div className="chart-container">
|
||||
@@ -1059,9 +1528,6 @@ export default function LiveTradingApp() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
{/* About Modal */}
|
||||
{showAboutModal && <AboutModal onClose={() => setShowAboutModal(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Header from './Header.jsx';
|
||||
|
||||
export default function AboutModal({ onClose }) {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const [language] = useState('zh');
|
||||
|
||||
const handleClose = () => {
|
||||
setIsClosing(true);
|
||||
// Wait for animation to complete before actually closing
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 600); // Match animation duration
|
||||
};
|
||||
|
||||
const overlayStyle = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: '#ffffff',
|
||||
zIndex: 9999,
|
||||
animation: isClosing
|
||||
? 'collapseUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards'
|
||||
: 'expandDown 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transformOrigin: 'top center',
|
||||
overflowY: 'auto'
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
maxWidth: '900px',
|
||||
width: '90%',
|
||||
margin: '0 auto',
|
||||
textAlign: 'left',
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
color: '#000000',
|
||||
lineHeight: 1.8,
|
||||
fontSize: '14px',
|
||||
letterSpacing: '0.01em',
|
||||
padding: '60px 20px 80px',
|
||||
animation: isClosing
|
||||
? 'fadeOutContent 0.4s ease forwards'
|
||||
: 'fadeInContent 0.8s ease 0.3s backwards'
|
||||
};
|
||||
|
||||
const highlight = {
|
||||
color: '#615CED',
|
||||
fontWeight: 600
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
color: '#615CED',
|
||||
textDecoration: 'none',
|
||||
borderBottom: '1px solid #615CED',
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
const closeHintStyle = {
|
||||
marginTop: '50px',
|
||||
fontSize: '11px',
|
||||
color: '#999',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
const languageSwitchStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: '25px',
|
||||
marginTop: '10px',
|
||||
gap: '0px',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'IBM Plex Mono', monospace"
|
||||
};
|
||||
|
||||
const getLangStyle = (isActive) => ({
|
||||
padding: '3px 8px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
background: isActive ? '#000' : '#fff',
|
||||
color: isActive ? '#fff' : '#000',
|
||||
border: 'none'
|
||||
});
|
||||
|
||||
const content = {
|
||||
en: {
|
||||
|
||||
question: "What happens if AI models don't compete with each other, but instead trade like a ",
|
||||
questionHighlight: "well-coordinated, high-performance team",
|
||||
questionEnd: "?",
|
||||
|
||||
intro: "Not arena, but TEAM. We Hope that AI is no longer entering the financial markets as isolated models—it is stepping in as ",
|
||||
introHighlight1: "teams",
|
||||
introContinue: ", collaborating in one of the most challenging and noise-filled ",
|
||||
introHighlight2: "real-time environments",
|
||||
introContinue2: ".",
|
||||
|
||||
|
||||
point1Highlight: "✦ Complementary skills",
|
||||
point1: " - across multiple agents—data analysis, strategy generation, risk management—working together like a real trading desk, exchanging information through notifications and meetings.",
|
||||
|
||||
point2Highlight: "✦ An agent system that continually evolves",
|
||||
point2: " — with memory modules that retain experience, learn from market feedback, reflect, and develop their own methodology over time.",
|
||||
|
||||
point3Highlight: "✦ AI teams interacting with live markets",
|
||||
point3: " — learning from real-time data and making immediate decisions, not just theoretical simulations."
|
||||
},
|
||||
zh: {
|
||||
intro: "如果不是让模型彼此竞争,而是像一支高效协作的团队一样进行实时交易,会发生什么?",
|
||||
question: "这里不是竞技场,而是团队。我们希望Agents不再单打独斗,而是「组团」进入实时金融市场——这一十分困难且充满噪声的环境。",
|
||||
trying: "我们正在探索多智能体协作在实时金融交易中的可能性。",
|
||||
|
||||
title1: "✦ 多智能体的技能互补",
|
||||
point1: "不同模型、不同角色的智能体像真实的金融团队一样协作,各自承担数据分析、策略生成、风险控制等职责。",
|
||||
point1Sub: "通过通知和会议机制进行信息交换,实现高效协作。",
|
||||
|
||||
title2: "✦ 能够持续进化的智能体系统",
|
||||
point2: "依托「记忆」模块,每个智能体都能跨回合保留经验,不断学习、反思与调整。我们希望能看到在长期实时交易中,Agent形成自己的独特方法论,而不是一次性偶然的推理。",
|
||||
point2Sub: "ReMe 记忆框架帮助 Agents 持续改进。",
|
||||
|
||||
title3: "✦ 实时参与市场的 AI Agents",
|
||||
point3: "Agents从实时行情中学习,并给予即时决策;不是纸上谈兵,而是面对市场的真实波动。"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes expandDown {
|
||||
from {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes collapseUp {
|
||||
from {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInContent {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutContent {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div style={overlayStyle} onClick={handleClose}>
|
||||
{/* Header */}
|
||||
<div className="header" style={{
|
||||
animation: isClosing
|
||||
? 'fadeOutContent 0.4s ease forwards'
|
||||
: 'fadeInContent 0.8s ease 0.3s backwards'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
<Header
|
||||
onEvoTradersClick={handleClose}
|
||||
evoTradersLinkStyle="close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={contentStyle} onClick={(e) => e.stopPropagation()}>
|
||||
<div style={languageSwitchStyle}>
|
||||
<span
|
||||
style={getLangStyle(true)}
|
||||
>
|
||||
中文
|
||||
</span>
|
||||
</div>
|
||||
|
||||
// Chinese Content
|
||||
<>
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
{content.zh.intro}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '40px', fontSize: '15px', fontWeight: 600 }}>
|
||||
{content.zh.question}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px', fontSize: '14px', opacity: 0.8 }}>
|
||||
{content.zh.trying}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ ...highlight, marginBottom: '10px' }}>
|
||||
{content.zh.title1}
|
||||
</div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{content.zh.point1}
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.7 }}>
|
||||
{content.zh.point1Sub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ ...highlight, marginBottom: '10px' }}>
|
||||
{content.zh.title2}
|
||||
</div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{content.zh.point2}
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.7 }}>
|
||||
{content.zh.point2Sub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<div style={{ ...highlight, marginBottom: '10px' }}>
|
||||
{content.zh.title3}
|
||||
</div>
|
||||
<div>
|
||||
{content.zh.point3}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px', opacity: 0.7 }}>
|
||||
我们已经在 GitHub 上开源。
|
||||
</div>
|
||||
<div style={{ marginBottom: '25px', opacity: 0.7 }}>
|
||||
EvoTraders 基于{' '}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
AgentScope
|
||||
</a>
|
||||
{' '}搭建,并使用其中的{' '}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
ReMe
|
||||
</a>
|
||||
{' '}作为记忆管理核心。
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px', fontSize: '14px' }}>
|
||||
你可以在此找到完整项目与示例:
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div style={{ marginTop: '40px' }}>
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/agentscope-samples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
github.com/agentscope-ai/agentscope-samples
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={closeHintStyle} onClick={handleClose}>
|
||||
点击此处关闭
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -149,7 +149,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
||||
// Get current selection display info
|
||||
const getCurrentSelectionInfo = () => {
|
||||
if (selectedAgent === 'all') {
|
||||
return { label: 'All Agents', modelInfo: null };
|
||||
return { label: '全部角色', modelInfo: null };
|
||||
}
|
||||
const agentInfo = getAgentInfoByName(selectedAgent);
|
||||
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
|
||||
@@ -191,7 +191,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard }, ref) => {
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>全部 Agents</span>
|
||||
<span>全部角色</span>
|
||||
</div>
|
||||
{uniqueAgents.map(agent => {
|
||||
const agentInfo = getAgentInfoByName(agent);
|
||||
@@ -419,17 +419,14 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<span
|
||||
style={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }}
|
||||
>
|
||||
<img
|
||||
src={ASSETS.remeLogo}
|
||||
alt="ReMe"
|
||||
alt="Memory"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
cursor: 'default',
|
||||
height: '12px',
|
||||
width: 'auto',
|
||||
objectFit: 'contain',
|
||||
@@ -449,9 +446,9 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
||||
lineHeight: 1,
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
↗
|
||||
MEMORY
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<span style={{
|
||||
background: 'linear-gradient(90deg, #00C2FF 0%, #5C4CE0 100%)',
|
||||
@@ -497,10 +494,10 @@ function MemoryItem({ memory, itemId, isHighlighted }) {
|
||||
color: 'transparent',
|
||||
display: 'inline-block'
|
||||
}}>
|
||||
Memory powered by AgentScope-ReMe
|
||||
Runtime Memory Layer
|
||||
</div>
|
||||
<div style={{ color: '#475569', opacity: 0.9 }}>
|
||||
Not only retrieves historical memories but also generates suggestions and hints for the current task based on latest context.
|
||||
Retrieves relevant historical context and produces guidance for the current task based on the latest conversation state.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,253 +1,29 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Header Component
|
||||
* Reusable header brand with EvoTraders logo, GitHub link, and Contact Us section
|
||||
*
|
||||
* @param {Function} onEvoTradersClick - Optional callback when EvoTraders is clicked
|
||||
* @param {string} evoTradersLinkStyle - Optional style variant: 'default' | 'close'
|
||||
* Reusable header brand for EvoTraders.
|
||||
*/
|
||||
export default function Header({
|
||||
onEvoTradersClick = null,
|
||||
evoTradersLinkStyle = 'default' // 'default' shows ↗, 'close' shows ↙
|
||||
}) {
|
||||
const [activeContactCard, setActiveContactCard] = useState({ yue: false, jiaji: false });
|
||||
const [clickedContactCard, setClickedContactCard] = useState(null);
|
||||
|
||||
const handleEvoTradersClick = () => {
|
||||
if (onEvoTradersClick) {
|
||||
onEvoTradersClick();
|
||||
}
|
||||
};
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="header-title" style={{ flex: '0 1 auto', minWidth: 0 }}>
|
||||
<span
|
||||
className="header-link"
|
||||
onClick={handleEvoTradersClick}
|
||||
style={{ cursor: 'pointer', padding: '4px 8px', borderRadius: '3px', display: 'inline-flex', alignItems: 'center', gap: '8px' }}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '3px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/trading_logo.png"
|
||||
alt="EvoTraders Logo"
|
||||
style={{ height: '24px', width: 'auto' }}
|
||||
/>
|
||||
EvoTraders {evoTradersLinkStyle === 'close' ? (
|
||||
<span className="link-arrow">↙</span>
|
||||
) : (
|
||||
<span className="link-arrow">↗</span>
|
||||
)}
|
||||
EvoTraders
|
||||
</span>
|
||||
|
||||
<span style={{
|
||||
width: '2px',
|
||||
height: '16px',
|
||||
background: '#666',
|
||||
margin: '0 16px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle'
|
||||
}} />
|
||||
|
||||
<span style={{
|
||||
padding: '1px 5px',
|
||||
fontSize: '9px',
|
||||
fontWeight: 700,
|
||||
color: '#00C853',
|
||||
background: 'rgba(0, 200, 83, 0.1)',
|
||||
border: '1px solid #00C853',
|
||||
borderRadius: '3px',
|
||||
letterSpacing: '0.5px',
|
||||
marginRight: '0px'
|
||||
}}>
|
||||
开源
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/agentscope-samples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="header-link"
|
||||
style={{ display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: '6px' }}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span>agentscope-samples</span>
|
||||
<span className="link-arrow">↗</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="header-link"
|
||||
style={{ display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: '6px', marginLeft: '0px' }}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span>agentscope-ReMe</span>
|
||||
<span className="link-arrow">↗</span>
|
||||
</a>
|
||||
|
||||
<span style={{
|
||||
width: '2px',
|
||||
height: '16px',
|
||||
background: '#666',
|
||||
margin: '0 16px',
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle'
|
||||
}} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
const bothActive = activeContactCard.yue && activeContactCard.jiaji;
|
||||
if (!bothActive) {
|
||||
setActiveContactCard({ yue: true, jiaji: true });
|
||||
setClickedContactCard('both');
|
||||
} else {
|
||||
setActiveContactCard({ yue: false, jiaji: false });
|
||||
setClickedContactCard(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="header-link">
|
||||
联系我们
|
||||
</span>
|
||||
|
||||
{/* Two contact buttons */}
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeContactCard.yue) {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: false }));
|
||||
if (clickedContactCard === 'yue' || clickedContactCard === 'both') {
|
||||
setClickedContactCard(null);
|
||||
}
|
||||
} else {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: true }));
|
||||
setClickedContactCard('yue');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!clickedContactCard || clickedContactCard === 'yue' || clickedContactCard === 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: true }));
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (clickedContactCard !== 'yue' && clickedContactCard !== 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, yue: false }));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: activeContactCard.yue ? '#615CED' : '#f5f5f5',
|
||||
color: activeContactCard.yue ? '#fff' : '#333',
|
||||
border: '1px solid',
|
||||
borderColor: activeContactCard.yue ? '#615CED' : '#e0e0e0',
|
||||
borderRadius: '3px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
letterSpacing: '0.5px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
maxWidth: activeContactCard.yue ? '80px' : '32px',
|
||||
minWidth: activeContactCard.yue ? '80px' : '32px'
|
||||
}}
|
||||
>
|
||||
{activeContactCard.yue ? (
|
||||
<a
|
||||
href="https://1mycell.github.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Yue Wu ↗
|
||||
</a>
|
||||
) : 'YW'}
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeContactCard.jiaji) {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: false }));
|
||||
if (clickedContactCard === 'jiaji' || clickedContactCard === 'both') {
|
||||
setClickedContactCard(null);
|
||||
}
|
||||
} else {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: true }));
|
||||
setClickedContactCard('jiaji');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!clickedContactCard || clickedContactCard === 'jiaji' || clickedContactCard === 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: true }));
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (clickedContactCard !== 'jiaji' && clickedContactCard !== 'both') {
|
||||
setActiveContactCard(prev => ({ ...prev, jiaji: false }));
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
background: activeContactCard.jiaji ? '#615CED' : '#f5f5f5',
|
||||
color: activeContactCard.jiaji ? '#fff' : '#333',
|
||||
border: '1px solid',
|
||||
borderColor: activeContactCard.jiaji ? '#615CED' : '#e0e0e0',
|
||||
borderRadius: '3px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
letterSpacing: '0.5px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
maxWidth: activeContactCard.jiaji ? '100px' : '32px',
|
||||
minWidth: activeContactCard.jiaji ? '100px' : '32px'
|
||||
}}
|
||||
>
|
||||
{activeContactCard.jiaji ? (
|
||||
<a
|
||||
href="https://dengjiaji.github.io/self/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Jiaji Deng ↗
|
||||
</a>
|
||||
) : 'JD'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
||||
fontFamily: '"Courier New", monospace',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
NO DATA AVAILABLE
|
||||
暂无图表数据
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -828,4 +828,3 @@ export default function NetValueChart({ equity, baseline, baseline_vw, momentum,
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { LLM_MODEL_LOGOS } from '../config/constants';
|
||||
|
||||
export default function RulesView() {
|
||||
const [language] = useState('zh');
|
||||
const [scale, setScale] = useState(1);
|
||||
const containerRef = useRef(null);
|
||||
const contentRef = useRef(null);
|
||||
|
||||
// Auto-scale content to fit container without scrolling
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (containerRef.current && contentRef.current) {
|
||||
const containerHeight = containerRef.current.clientHeight;
|
||||
const contentHeight = contentRef.current.scrollHeight;
|
||||
|
||||
if (contentHeight > containerHeight) {
|
||||
const newScale = containerHeight / contentHeight;
|
||||
setScale(Math.max(newScale * 0.95, 0.5)); // Min scale 0.5, with 95% of available space
|
||||
} else {
|
||||
setScale(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial resize
|
||||
handleResize();
|
||||
|
||||
// Listen to window resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Observe content changes
|
||||
const observer = new ResizeObserver(handleResize);
|
||||
if (contentRef.current) {
|
||||
observer.observe(contentRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [language]);
|
||||
|
||||
const containerStyle = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#FFFFFF',
|
||||
padding: '10px'
|
||||
};
|
||||
|
||||
const contentWrapperStyle = {
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'center center',
|
||||
transition: 'transform 0.3s ease',
|
||||
width: '100%',
|
||||
maxWidth: '900px'
|
||||
};
|
||||
|
||||
const innerContentStyle = {
|
||||
color: '#000000',
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.6',
|
||||
letterSpacing: '0.01em',
|
||||
padding: '0 10px'
|
||||
};
|
||||
|
||||
const highlight = {
|
||||
color: '#000000',
|
||||
fontWeight: 700
|
||||
};
|
||||
|
||||
const sectionTitleStyle = {
|
||||
color: '#615CED',
|
||||
fontSize: '16px',
|
||||
fontWeight: 700,
|
||||
marginBottom: '8px',
|
||||
marginTop: '12px',
|
||||
marginLeft: '-10px',
|
||||
marginRight: '-10px',
|
||||
width: 'calc(100% + 20px)',
|
||||
padding: '8px 10px',
|
||||
backgroundColor: '#FFFFFF',
|
||||
letterSpacing: '0.5px',
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
|
||||
const subsectionStyle = {
|
||||
marginBottom: '8px',
|
||||
paddingLeft: '10px',
|
||||
borderLeft: '2px solid #CCCCCC'
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
color: '#615CED',
|
||||
textDecoration: 'none',
|
||||
borderBottom: '1px solid #615CED',
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
const languageSwitchStyle = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px',
|
||||
gap: '0px',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'IBM Plex Mono', monospace"
|
||||
};
|
||||
|
||||
const getLangStyle = (isActive) => ({
|
||||
padding: '4px 10px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
background: isActive ? '#000000' : 'transparent',
|
||||
color: isActive ? '#FFFFFF' : '#666666',
|
||||
border: 'none',
|
||||
borderRadius: '2px'
|
||||
});
|
||||
|
||||
const llmLogos = [
|
||||
{ name: 'Alibaba', file: 'Alibaba.jpeg', label: 'Qwen', url: LLM_MODEL_LOGOS['Alibaba'] },
|
||||
{ name: 'DeepSeek', file: 'DeepSeek.png', label: 'DeepSeek', url: LLM_MODEL_LOGOS['DeepSeek'] },
|
||||
{ name: 'Moonshot', file: 'Moonshot.jpeg', label: 'Moonshot', url: LLM_MODEL_LOGOS['Moonshot'] },
|
||||
{ name: 'Zhipu AI', file: 'Zhipu AI.png', label: 'Zhipu AI', url: LLM_MODEL_LOGOS['Zhipu AI'] }
|
||||
];
|
||||
|
||||
const content = {
|
||||
en: {
|
||||
section1Title: "Agent Setup",
|
||||
pmRole: "Portfolio Manager",
|
||||
pmDesc: "Makes final trading decisions and orchestrates team collaboration",
|
||||
rmRole: "Risk Manager",
|
||||
rmDesc: "Monitors portfolio risk and enforces risk limits",
|
||||
analystsRole: "Analysts",
|
||||
analystsDesc: "Conduct specialized research with different tools and AI models:",
|
||||
analysts: [
|
||||
{ name: "Valuation Analyst", model: "Moonshot", modelKey: "Moonshot" },
|
||||
{ name: "Sentiment Analyst", model: "Qwen", modelKey: "Alibaba" },
|
||||
{ name: "Fundamentals Analyst", model: "DeepSeek", modelKey: "DeepSeek" },
|
||||
{ name: "Technical Analyst", model: "Zhipu AI", modelKey: "Zhipu AI" }
|
||||
],
|
||||
|
||||
section2Title: "Agent Decision Mechanism",
|
||||
|
||||
tradingProcess: "Daily Trading Process",
|
||||
tradingDesc: "Agents trade on a daily frequency while continuously tracking portfolio performance. Before each day's final trading decision, agents go through three key phases:",
|
||||
|
||||
analysisPhase: "• Analysis Phase",
|
||||
analysisDesc: "All agents independently analyze information and form judgments based on their specialized tools.",
|
||||
|
||||
communicationPhase: "• Communication Phase",
|
||||
commIntro: "Multiple communication channels enable effective collaboration: 1v1 Private Chat / 1vN Notification / NvN Conference",
|
||||
|
||||
decisionPhase: "• Decision Phase",
|
||||
decisionDesc: "Portfolio Manager aggregates all information and makes the final team trading decision. The original trading signals from analysts are only used for individual-level ranking.",
|
||||
|
||||
reflectionTitle: "Learning & Evolution",
|
||||
reflectionDesc: "Agents reflect on daily investment performance, summarize insights, and store them in ",
|
||||
remeLink: "ReMe",
|
||||
reflectionDesc2: " memory framework for continuous improvement.",
|
||||
|
||||
section3Title: "Performance Evaluation",
|
||||
|
||||
chartTitle: "• Performance Chart",
|
||||
chartDesc: "Track portfolio equity curve vs. benchmarks (equal-weight, value-weighted, momentum). Use this to assess overall strategy effectiveness.",
|
||||
|
||||
rankingTitle: "• Analyst Rankings",
|
||||
rankingDesc: "Click avatars in Trading Room to view analyst performance (Win Rate, Bull/Bear Win Rate). Use this to understand which analysts provide the most valuable insights.",
|
||||
|
||||
statsTitle: "• Statistics",
|
||||
statsDesc: "Detailed holdings and trade history. Use this for in-depth analysis of position management and execution quality.",
|
||||
|
||||
callToAction: "Fork on ",
|
||||
repoLink: "GitHub",
|
||||
callToAction2: " to customize!"
|
||||
},
|
||||
zh: {
|
||||
section1Title: "Agent 设定",
|
||||
pmRole: "投资经理",
|
||||
pmDesc: "负责最终交易决策和团队协作",
|
||||
rmRole: "风控经理",
|
||||
rmDesc: "监控组合风险并执行风险限制",
|
||||
analystsRole: "分析师",
|
||||
analystsDesc: "使用不同工具和 AI 模型进行专业研究:",
|
||||
analysts: [
|
||||
{ name: "估值分析师", model: "Moonshot", modelKey: "Moonshot" },
|
||||
{ name: "情绪分析师", model: "Qwen", modelKey: "Alibaba" },
|
||||
{ name: "基本面分析师", model: "DeepSeek", modelKey: "DeepSeek" },
|
||||
{ name: "技术分析师", model: "Zhipu AI", modelKey: "Zhipu AI" }
|
||||
],
|
||||
|
||||
section2Title: "Agent 决策机制",
|
||||
|
||||
tradingProcess: "交易流程",
|
||||
tradingDesc: "智能体以日频进行交易并持续跟踪组合净值。每天最终交易决策前,会经历三个关键阶段:",
|
||||
|
||||
analysisPhase: "• 分析阶段",
|
||||
analysisDesc: "所有智能体根据各自的工具和信息独立分析并形成判断。",
|
||||
|
||||
communicationPhase: "• 沟通阶段",
|
||||
commIntro: "提供了多种沟通渠道:1v1 私聊 / 1vN 通知 / NvN 会议",
|
||||
|
||||
decisionPhase: "• 决策阶段",
|
||||
decisionDesc: "由投资经理汇总所有信息,并给出最终的团队交易决策。分析师给出的原始交易信号仅用于个人维度排名。",
|
||||
|
||||
reflectionTitle: "学习与进化",
|
||||
reflectionDesc: "智能体根据当日实际收益反思决策、总结经验,并存入 ",
|
||||
remeLink: "ReMe",
|
||||
reflectionDesc2: " 记忆框架以持续改进。",
|
||||
|
||||
section3Title: "收益评估",
|
||||
|
||||
chartTitle: "• 业绩图表",
|
||||
chartDesc: "追踪组合收益曲线 vs. 基准策略(等权、市值加权、动量)。用于评估整体策略有效性。",
|
||||
|
||||
rankingTitle: "• 分析师排名",
|
||||
rankingDesc: "在交易室点击头像查看分析师表现(胜率、牛/熊市胜率),用来了解哪些分析师提供了最有价值的洞察。",
|
||||
|
||||
statsTitle: "• 统计数据",
|
||||
statsDesc: "详细的持仓和交易历史。用于深入分析仓位管理和执行质量。",
|
||||
|
||||
callToAction: "可在 ",
|
||||
repoLink: "GitHub",
|
||||
callToAction2: " 上 Fork 并自行定制。"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={containerStyle}>
|
||||
<div ref={contentRef} style={contentWrapperStyle}>
|
||||
<div style={innerContentStyle}>
|
||||
<div style={languageSwitchStyle}>
|
||||
<span
|
||||
style={getLangStyle(true)}
|
||||
>
|
||||
中文
|
||||
</span>
|
||||
</div>
|
||||
|
||||
// Chinese Content
|
||||
<>
|
||||
{/* 第一部分:Agent 设定 */}
|
||||
<div style={sectionTitleStyle}>{content.zh.section1Title}</div>
|
||||
|
||||
{/* 角色 */}
|
||||
<div style={{ marginBottom: '8px', fontSize: '12px' }}>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.pmRole}:</span> {content.zh.pmDesc}
|
||||
</div>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.rmRole}:</span> {content.zh.rmDesc}
|
||||
</div>
|
||||
<div style={{ marginBottom: '3px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.analystsRole}:</span> {content.zh.analystsDesc}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analysts 与 AI 模型 */}
|
||||
<div style={{ marginLeft: '10px', marginBottom: '8px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px 14px', fontSize: '11px' }}>
|
||||
{content.zh.analysts.map(analyst => {
|
||||
const logo = llmLogos.find(l => l.name === analyst.modelKey);
|
||||
return (
|
||||
<div key={analyst.name} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{logo && (
|
||||
<img
|
||||
src={logo.url}
|
||||
alt={logo.label}
|
||||
style={{
|
||||
height: '16px',
|
||||
width: 'auto',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span style={{ fontWeight: 600 }}>{analyst.name}</span>
|
||||
<span style={{ color: '#666' }}>- {analyst.model}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px', fontSize: '11px', fontStyle: 'italic', opacity: 0.8 }}>
|
||||
{content.zh.callToAction}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/agentscope-samples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
{content.zh.repoLink}
|
||||
</a>
|
||||
{content.zh.callToAction2}
|
||||
</div>
|
||||
|
||||
{/* 第二部分:Agent 决策机制 */}
|
||||
<div style={sectionTitleStyle}>{content.zh.section2Title}</div>
|
||||
|
||||
<div style={{ marginBottom: '6px' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '3px' }}>{content.zh.tradingProcess}</div>
|
||||
<div style={{ marginBottom: '6px', fontSize: '12px' }}>{content.zh.tradingDesc}</div>
|
||||
|
||||
<div style={subsectionStyle}>
|
||||
<div style={{ marginBottom: '4px', fontSize: '12px' }}>
|
||||
<span style={highlight}>{content.zh.analysisPhase.replace('• ', '')}:</span> {content.zh.analysisDesc}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '4px', fontSize: '12px' }}>
|
||||
<span style={highlight}>{content.zh.communicationPhase.replace('• ', '')}:</span> {content.zh.commIntro}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
<span style={highlight}>{content.zh.decisionPhase.replace('• ', '')}:</span> {content.zh.decisionDesc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '3px' }}>{content.zh.reflectionTitle}</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
{content.zh.reflectionDesc}
|
||||
<a
|
||||
href="https://github.com/agentscope-ai/ReMe"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={linkStyle}
|
||||
>
|
||||
{content.zh.remeLink}
|
||||
</a>
|
||||
{content.zh.reflectionDesc2}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第三部分:收益评估 */}
|
||||
<div style={sectionTitleStyle}>{content.zh.section3Title}</div>
|
||||
<div style={subsectionStyle}>
|
||||
<div style={{ marginBottom: '3px', fontSize: '12px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.chartTitle.replace('• ', '')}:</span> {content.zh.chartDesc}
|
||||
</div>
|
||||
<div style={{ marginBottom: '3px', fontSize: '12px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.rankingTitle.replace('• ', '')}:</span> {content.zh.rankingDesc}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
<span style={{ fontWeight: 600 }}>{content.zh.statsTitle.replace('• ', '')}:</span> {content.zh.statsDesc}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1076
frontend/src/components/StockExplainView.jsx
Normal file
1076
frontend/src/components/StockExplainView.jsx
Normal file
File diff suppressed because it is too large
Load Diff
244
frontend/src/components/WatchlistPanel.jsx
Normal file
244
frontend/src/components/WatchlistPanel.jsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function WatchlistPanel({
|
||||
isOpen,
|
||||
isConnected,
|
||||
isSaving,
|
||||
draftSymbols,
|
||||
inputValue,
|
||||
feedback,
|
||||
suggestions,
|
||||
onToggle,
|
||||
onClose,
|
||||
onInputChange,
|
||||
onInputKeyDown,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onRestoreCurrent,
|
||||
onRestoreDefault,
|
||||
onSuggestionClick,
|
||||
onSave
|
||||
}) {
|
||||
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'
|
||||
}}
|
||||
>
|
||||
WATCHLIST
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(100% + 10px)',
|
||||
right: 0,
|
||||
width: 360,
|
||||
maxWidth: 'min(360px, 92vw)',
|
||||
padding: '14px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid #D9D9D9',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0 12px 36px rgba(0, 0, 0, 0.14)',
|
||||
zIndex: 40,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
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 }}>
|
||||
保存后会立即更新当前 run 的 watchlist
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: '#666666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
minHeight: 36,
|
||||
padding: '2px 0'
|
||||
}}>
|
||||
{draftSymbols.map((symbol) => (
|
||||
<button
|
||||
key={symbol}
|
||||
onClick={() => onRemove(symbol)}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 999,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#F7F9FB',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<span>{symbol}</span>
|
||||
<span style={{ color: '#777777' }}>×</span>
|
||||
</button>
|
||||
))}
|
||||
{draftSymbols.length === 0 && (
|
||||
<div style={{ fontSize: '11px', color: '#888888', padding: '8px 2px' }}>
|
||||
还没有股票,输入代码后回车添加
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
value={inputValue}
|
||||
onChange={(e) => onInputChange(e.target.value)}
|
||||
onKeyDown={onInputKeyDown}
|
||||
placeholder="输入股票代码,回车添加"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '9px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '12px',
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={onAdd}
|
||||
style={{
|
||||
padding: '9px 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#F7F9FB',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{suggestions.map((symbol) => {
|
||||
const active = draftSymbols.includes(symbol);
|
||||
return (
|
||||
<button
|
||||
key={symbol}
|
||||
onClick={() => onSuggestionClick(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', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={onRestoreCurrent}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复当前
|
||||
</button>
|
||||
<button
|
||||
onClick={onRestoreDefault}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D0D7DE',
|
||||
background: '#FFFFFF',
|
||||
color: '#111111',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,88 @@ import { AGENTS } from "../config/constants";
|
||||
|
||||
const MAX_FEED_ITEMS = 200;
|
||||
|
||||
const normalizeSystemContent = (content) => {
|
||||
if (typeof content !== "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (trimmed === "Runtime assets reloaded." || trimmed === "运行时配置已热更新") {
|
||||
return "配置已刷新";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("Watchlist updated:")) {
|
||||
const symbols = trimmed.replace("Watchlist updated:", "").trim();
|
||||
return symbols ? `自选已更新: ${symbols}` : "自选已更新";
|
||||
}
|
||||
|
||||
if (trimmed === "已连接实时数据服务") {
|
||||
return "已连接";
|
||||
}
|
||||
|
||||
if (trimmed === "正在尝试连接数据服务...") {
|
||||
return "连接中...";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("day_start:")) {
|
||||
const value = trimmed.replace("day_start:", "").trim();
|
||||
return value ? `交易日开始:${value}` : "交易日开始";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("day_complete:")) {
|
||||
const value = trimmed.replace("day_complete:", "").trim();
|
||||
return value ? `交易日结束:${value}` : "交易日结束";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("day_error:")) {
|
||||
const value = trimmed.replace("day_error:", "").trim();
|
||||
return value ? `交易日异常:${value}` : "交易日异常";
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const normalizeConferenceTitle = (title) => {
|
||||
if (typeof title !== "string") {
|
||||
return "投资讨论";
|
||||
}
|
||||
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) {
|
||||
return "投资讨论";
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("Investment Discussion -")) {
|
||||
const date = trimmed.replace("Investment Discussion -", "").trim();
|
||||
return date ? `投资讨论 · ${date}` : "投资讨论";
|
||||
}
|
||||
|
||||
if (trimmed === "Team Conference") {
|
||||
return "投资讨论";
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const normalizeAgentLabel = (agentName, agentId) => {
|
||||
if (typeof agentName === "string") {
|
||||
const trimmed = agentName.trim();
|
||||
if (trimmed.toLowerCase() === "conference summary") {
|
||||
return "会议总结";
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof agentId === "string" && agentId.trim().toLowerCase() === "conference summary") {
|
||||
return "会议总结";
|
||||
}
|
||||
|
||||
return agentName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a unique ID for feed items
|
||||
*/
|
||||
@@ -26,7 +108,7 @@ const eventToMessage = (evt) => {
|
||||
id: generateId("msg"),
|
||||
timestamp,
|
||||
agentId: evt.agentId,
|
||||
agent: agent?.name || evt.agentName || evt.agentId || "Agent",
|
||||
agent: normalizeAgentLabel(agent?.name || evt.agentName || evt.agentId || "Agent", evt.agentId),
|
||||
role: agent?.role || evt.role || "Agent",
|
||||
content: evt.content
|
||||
};
|
||||
@@ -50,7 +132,7 @@ const eventToMessage = (evt) => {
|
||||
timestamp,
|
||||
agent: "System",
|
||||
role: "System",
|
||||
content: evt.content || `${evt.type}: ${evt.date || ""}`
|
||||
content: normalizeSystemContent(evt.content || `${evt.type}: ${evt.date || ""}`)
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -129,7 +211,7 @@ export function useFeedProcessor() {
|
||||
// Start a new conference
|
||||
currentConference = {
|
||||
id: evt.conferenceId || generateId("conf"),
|
||||
title: evt.title || "Team Conference",
|
||||
title: normalizeConferenceTitle(evt.title || "Team Conference"),
|
||||
startTime: evt.timestamp || evt.ts || Date.now(),
|
||||
endTime: null,
|
||||
isLive: false,
|
||||
@@ -209,7 +291,7 @@ export function useFeedProcessor() {
|
||||
if (evt.type === "conference_start") {
|
||||
const conference = {
|
||||
id: evt.conferenceId || generateId("conf"),
|
||||
title: evt.title || "Team Conference",
|
||||
title: normalizeConferenceTitle(evt.title || "Team Conference"),
|
||||
startTime: evt.timestamp || evt.ts || Date.now(),
|
||||
endTime: null,
|
||||
isLive: true,
|
||||
@@ -312,7 +394,7 @@ export function useFeedProcessor() {
|
||||
timestamp: Date.now(),
|
||||
agent: "System",
|
||||
role: "System",
|
||||
content
|
||||
content: normalizeSystemContent(content)
|
||||
};
|
||||
|
||||
const activeConf = activeConferenceRef.current;
|
||||
|
||||
@@ -1030,8 +1030,9 @@ export default function GlobalStyles() {
|
||||
/* Three-view slider (Room / Chart / Statistics) */
|
||||
.view-slider-three {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
height: calc(100% - 40px);
|
||||
display: flex;
|
||||
transition: transform 1.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@@ -1052,7 +1053,7 @@ export default function GlobalStyles() {
|
||||
transform: translateX(-66.666%);
|
||||
}
|
||||
|
||||
/* Four-view slider (Rules / Room / Chart / Statistics) */
|
||||
/* Four-view slider (Room / Explain / Chart / Statistics) */
|
||||
.view-slider-four {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
@@ -1066,11 +1067,11 @@ export default function GlobalStyles() {
|
||||
transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.view-slider-four.show-rules {
|
||||
.view-slider-four.show-room {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.view-slider-four.show-room {
|
||||
.view-slider-four.show-explain {
|
||||
transform: translateX(-25%);
|
||||
}
|
||||
|
||||
@@ -1873,4 +1874,3 @@ export default function GlobalStyles() {
|
||||
`}</style>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user