ui: tighten dashboard copy and fix layout issues

This commit is contained in:
k
2026-06-14 21:58:56 +08:00
parent 4c3c8c6f87
commit 8534d90589
19 changed files with 1036 additions and 345 deletions
+71 -45
View File
@@ -238,43 +238,59 @@ export default function AnalyticsPageClient() {
<span style={{ fontSize: 12, color: 'var(--ink-4)' }}>{accuracy.total_directional_signals} directional signals measured</span>
<span style={{ fontSize: 11, color: 'var(--up)', fontWeight: 600, marginLeft: 'auto' }}>Public · no login needed</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 8 }}>
{/* Overall */}
<div style={{ background: 'var(--surface-2)', borderRadius: 10, padding: '12px 14px' }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink-3)', marginBottom: 8, textTransform: 'uppercase', letterSpacing: '0.06em' }}>Overall</div>
{(['m5','m15','m1h'] as const).map(w => {
const d = accuracy.overall[w]
const pct = d.accuracy_pct
const color = pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)'
return (
<div key={w} className="row between" style={{ marginBottom: 3 }}>
<span style={{ fontSize: 11, color: 'var(--ink-3)' }}>{w.replace('m1','1').replace('m','')}</span>
<span style={{ fontSize: 13, fontWeight: 700, color, fontVariantNumeric: 'tabular-nums' }}>{pct != null ? `${pct.toFixed(0)}%` : '—'}</span>
</div>
)
})}
</div>
{Object.entries(accuracy.by_signal).map(([sig, data]) => {
const label = sig === 'buy' ? '🟢 Buy' : sig === 'short' ? '🔴 Short' : '🟡 Sell'
return (
<div key={sig} style={{ background: 'var(--surface-2)', borderRadius: 10, padding: '12px 14px' }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink-3)', marginBottom: 8, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{label} <span style={{ fontWeight: 400 }}>({data.count})</span></div>
{(['m5','m15','m1h'] as const).map(w => {
const d = data[w]
if (d.checked < 2) return <div key={w} className="row between" style={{ marginBottom: 3 }}><span style={{ fontSize: 11, color: 'var(--ink-3)' }}>{w.replace('m1','1').replace('m','')}</span><span style={{ fontSize: 12, color: 'var(--ink-3)' }}></span></div>
const pct = d.accuracy_pct
const color = pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)'
return (
<div key={w} className="row between" style={{ marginBottom: 3 }}>
<span style={{ fontSize: 11, color: 'var(--ink-3)' }}>{w.replace('m1','1').replace('m','')}</span>
<span style={{ fontSize: 13, fontWeight: 700, color, fontVariantNumeric: 'tabular-nums' }}>{pct.toFixed(0)}%</span>
{(() => {
// One full-width block per measurement window. Big overall number
// + a hit-rate bar (50% tick = coin-flip baseline) + per-direction
// breakdown. `repeat(3, 1fr)` stretches across the card so no
// whitespace is stranded on wide screens.
const WINDOWS = ['m5', 'm15', 'm1h'] as const
const WINDOW_LABEL = { m5: 'After 5 min', m15: 'After 15 min', m1h: 'After 1 hour' } as const
const pctColor = (pct: number) => pct >= 55 ? 'var(--up)' : pct >= 45 ? 'var(--ink-2)' : 'var(--down)'
const fmtBreakdown = (pct: number | null, sparse: boolean) =>
sparse || pct == null || Number.isNaN(pct) ? '—' : `${pct.toFixed(0)}%`
// auto-fit, not repeat(3,1fr) — a hard 3-col grid overflowed the
// viewport on phones and forced the whole page to scroll sideways.
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))', gap: 12 }}>
{WINDOWS.map(w => {
const pct = accuracy.overall[w].accuracy_pct
const has = pct != null && !Number.isNaN(pct)
return (
<div key={w} style={{ background: 'var(--surface-2)', borderRadius: 10, padding: '14px 16px' }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.06em', display: 'flex', alignItems: 'center' }}>
{WINDOW_LABEL[w]}
<InfoTip text={`Share of signals where price had moved in the signalled direction ${w === 'm5' ? '5 minutes' : w === 'm15' ? '15 minutes' : '1 hour'} after the post. 50% = coin flip.`} placement="top" />
</div>
)
})}
</div>
)
})}
</div>
<div className="mono" style={{ fontSize: 30, fontWeight: 600, letterSpacing: '-0.01em', marginTop: 8, color: has ? pctColor(pct) : 'var(--ink-4)', fontVariantNumeric: 'tabular-nums' }}>
{has ? `${pct.toFixed(0)}%` : '—'}
</div>
{/* hit-rate bar with a 50% coin-flip tick */}
<div style={{ position: 'relative', height: 6, background: 'var(--surface-3)', borderRadius: 999, marginTop: 10, overflow: 'hidden' }}>
{has && (
<div style={{ position: 'absolute', inset: 0, width: `${Math.min(100, Math.max(0, pct))}%`, background: pctColor(pct), borderRadius: 999, opacity: 0.75 }} />
)}
<div style={{ position: 'absolute', left: '50%', top: 0, bottom: 0, width: 1.5, background: 'var(--ink-4)', opacity: 0.55 }} />
</div>
<div style={{ display: 'flex', gap: 16, marginTop: 12, fontSize: 12, color: 'var(--ink-3)', flexWrap: 'wrap' }}>
{Object.entries(accuracy.by_signal).map(([sig, d]) => {
const sparse = d[w].checked < 2
const v = fmtBreakdown(d[w].accuracy_pct, sparse)
const label = sig === 'buy' ? '🟢 Buy' : sig === 'short' ? '🔴 Short' : '🟡 Sell'
return (
<span key={sig} style={{ whiteSpace: 'nowrap' }}>
{label}{' '}
<strong style={{ color: sparse ? 'var(--ink-4)' : 'var(--ink-2)', fontVariantNumeric: 'tabular-nums' }}>{v}</strong>
<span style={{ color: 'var(--ink-4)' }}> ({d.count})</span>
</span>
)
})}
</div>
</div>
)
})}
</div>
)
})()}
</div>
)}
@@ -315,10 +331,21 @@ export default function AnalyticsPageClient() {
<div className="hero-value mono" style={{ marginTop: 6, fontSize: 40 }}>
{filteredTrades.length ? fmtMoney(summaryPnl) : '—'}
</div>
<div className="row gap-s" style={{ marginTop: 8 }}>
<span className={`chip ${winRate >= 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% {isZh ? '胜率' : 'win rate'}</span>
<span className="chip">{isZh ? `${filteredTrades.length} 笔交易` : `${filteredTrades.length} trades`}</span>
</div>
{/* Win-rate chip only when there are trades to rate — a red
"0.0% win rate" next to an em-dash P&L misreads as a losing
record when the wallet simply has no data. */}
{pricedTrades.length > 0 && (
<div className="row gap-s" style={{ marginTop: 8 }}>
<span className={`chip ${winRate >= 50 ? 'up' : 'down'}`}>{winRate.toFixed(1)}% {isZh ? '胜率' : 'win rate'}</span>
</div>
)}
{filteredTrades.length === 0 && (
<div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 12, lineHeight: 1.6, maxWidth: 520 }}>
No closed bot trades in this window. P&amp;L, win rate and the
per-trade metrics below fill in once auto-trade executions close
or pick a longer window on the top right.
</div>
)}
</div>
</div>
</div>
@@ -339,15 +366,14 @@ export default function AnalyticsPageClient() {
{/* Signal accuracy is now shown at the top of the page (public, no login needed).
Removed duplicate block here to avoid showing the same data twice. */}
{!filteredTrades.length && (
{/* Empty-state card renders only for states no other element explains:
- "window empty but trades exist" → Performance hero's inline note
- privateLocked → the unlock card at the top of the page */}
{!filteredTrades.length && !privateLocked && (!isConnected || !address || !trades.length) && (
<div className="card" style={{ padding: 60, textAlign: 'center', color: 'var(--ink-3)' }}>
<p style={{ fontSize: 14 }}>
{!isConnected || !address
? (isZh ? '连接钱包以加载你的分析数据。' : 'Connect your wallet to load your analytics.')
: privateLocked
? (isZh ? '签名解锁后可查看你的真实业绩和交易历史。' : 'Sign once above to unlock your personal P&L and trade history.')
: trades.length
? (isZh ? `${period} 时间窗内还没有已平仓交易。` : `No trades closed in the ${period} window yet.`)
: (isZh ? '还没有交易数据。机器人开始执行后,这里会自动出现统计。' : 'No trade data yet. The bot will populate analytics once it starts executing.')}
</p>
</div>