diff --git a/web/src/App.css b/web/src/App.css index 4af5b70..8d37801 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -293,53 +293,6 @@ body { border-radius: 10px; } -.opportunity-card-sidebar { - background: var(--bg-card); - padding: 20px; - border-radius: 12px; - border: 1px solid var(--border); -} - -.opportunity-card-sidebar h3 { - margin: 0 0 16px 0; - font-size: 0.95rem; - text-transform: uppercase; - color: var(--text-dim); - letter-spacing: 1px; -} - -.opp-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.opp-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - background: color-mix(in oklab, var(--text-main) 2%, transparent); - border-radius: 6px; - font-size: 0.85rem; -} - -.opp-symbol { - font-weight: 700; -} - -.opp-value.ai { - color: var(--accent); -} - -.opp-empty { - text-align: center; - color: var(--text-dim); - font-style: italic; - font-size: 0.8rem; - padding: 10px; -} - .readiness-card { background: var(--bg-card); border-radius: 12px; diff --git a/web/src/components/ComponentsSmoke.test.ts b/web/src/components/ComponentsSmoke.test.ts index e9133fd..0907537 100644 --- a/web/src/components/ComponentsSmoke.test.ts +++ b/web/src/components/ComponentsSmoke.test.ts @@ -145,11 +145,11 @@ describe('component smoke coverage', () => { it('renders market opportunity and ticker components', () => { const volatileHtml = renderToStaticMarkup(React.createElement(TopVolatile, { botState })); - expect(volatileHtml).toContain('Top Volatile'); + expect(volatileHtml).toContain('Top movers'); expect(volatileHtml).toContain('BTC/USDT'); const aiHtml = renderToStaticMarkup(React.createElement(AISetups, { botState })); - expect(aiHtml).toContain('Best AI Setups'); + expect(aiHtml).toContain('AI setups'); expect(aiHtml).toContain('82% Conf'); const tickerHtml = renderToStaticMarkup(React.createElement(MarketTicker, { botState })); diff --git a/web/src/components/MarketOpportunities.dom.test.tsx b/web/src/components/MarketOpportunities.dom.test.tsx index 511b32f..169e9ba 100644 --- a/web/src/components/MarketOpportunities.dom.test.tsx +++ b/web/src/components/MarketOpportunities.dom.test.tsx @@ -40,10 +40,10 @@ describe('MarketOpportunities Components', () => { expect(screen.getByText('-2.10%')).toHaveClass('down'); }); - it('shows scanning when no symbols', () => { - render(); - expect(screen.getByText('Scanning...')).toBeInTheDocument(); - }); + it('shows scanning when no symbols', () => { + render(); + expect(screen.getByText('Waiting for live market ticks...')).toBeInTheDocument(); + }); }); describe('AISetups', () => { @@ -62,9 +62,9 @@ describe('MarketOpportunities Components', () => { symbols: { 'AAPL': { rules: {} } } - } as any; - render(); - expect(screen.getByText('No AI data yet...')).toBeInTheDocument(); - }); + } as any; + render(); + expect(screen.getByText('No AI confidence signals yet.')).toBeInTheDocument(); + }); }); }); diff --git a/web/src/components/MarketOpportunities.tsx b/web/src/components/MarketOpportunities.tsx index d3341e3..5ea3f0a 100644 --- a/web/src/components/MarketOpportunities.tsx +++ b/web/src/components/MarketOpportunities.tsx @@ -1,56 +1,71 @@ -import type { BotState } from '../hooks/useWebSocket'; +import type { BotState } from '../hooks/useWebSocket'; +import { Activity, BrainCircuit } from 'lucide-react'; interface MarketOpportunitiesProps { botState: BotState; } -export const TopVolatile = ({ botState }: MarketOpportunitiesProps) => { - const symbols = Object.keys(botState.symbols); - - return ( -
-

🔥 Top Volatile (24h)

-
- {symbols - .sort((a, b) => Math.abs(botState.symbols[b].change24h || 0) - Math.abs(botState.symbols[a].change24h || 0)) - .slice(0, 5) // Keep it concise for corner - .map(symbol => ( -
- {symbol} - = 0 ? 'up' : 'down'}`}> +export const TopVolatile = ({ botState }: MarketOpportunitiesProps) => { + const symbols = Object.keys(botState.symbols); + const topSymbols = symbols + .sort((a, b) => Math.abs(botState.symbols[b].change24h || 0) - Math.abs(botState.symbols[a].change24h || 0)) + .slice(0, 5); + + return ( +
+
+ +
+

Top movers

+

24h change by absolute move.

+
+
+
+ {topSymbols + .map(symbol => ( +
+ {symbol} + = 0 ? 'up' : 'down'}`}> {botState.symbols[symbol].change24h >= 0 ? '+' : ''}{botState.symbols[symbol].change24h?.toFixed(2)}%
))} - {symbols.length === 0 &&
Scanning...
} -
-
- ); -}; - -export const AISetups = ({ botState }: MarketOpportunitiesProps) => { - const symbols = Object.keys(botState.symbols); - - return ( -
-

🧠 Best AI Setups

-
- {symbols - .filter(s => botState.symbols[s].rules['AIAnalysisRule']?.metadata?.confidence !== undefined) - .sort((a, b) => (botState.symbols[b].rules['AIAnalysisRule']?.metadata?.confidence || 0) - (botState.symbols[a].rules['AIAnalysisRule']?.metadata?.confidence || 0)) - .slice(0, 5) - .map(symbol => ( -
- {symbol} - + {symbols.length === 0 &&
Waiting for live market ticks...
} +
+ + ); +}; + +export const AISetups = ({ botState }: MarketOpportunitiesProps) => { + const symbols = Object.keys(botState.symbols); + const aiSymbols = symbols + .filter(s => botState.symbols[s].rules['AIAnalysisRule']?.metadata?.confidence !== undefined) + .sort((a, b) => (botState.symbols[b].rules['AIAnalysisRule']?.metadata?.confidence || 0) - (botState.symbols[a].rules['AIAnalysisRule']?.metadata?.confidence || 0)) + .slice(0, 5); + + return ( +
+
+ +
+

AI setups

+

Highest-confidence symbols from strategy rules.

+
+
+
+ {aiSymbols + .map(symbol => ( +
+ {symbol} + {botState.symbols[symbol].rules['AIAnalysisRule']?.metadata?.confidence}% Conf
))} - {symbols.filter(s => botState.symbols[s].rules['AIAnalysisRule']?.metadata?.confidence !== undefined).length === 0 && ( -
No AI data yet...
- )} -
-
- ); -}; + {aiSymbols.length === 0 && ( +
No AI confidence signals yet.
+ )} +
+ + ); +}; diff --git a/web/src/index.css b/web/src/index.css index 2c2c395..109550f 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1767,6 +1767,119 @@ body { min-width: 0; } +.opportunity-card-sidebar { + display: grid; + gap: 18px; + min-width: 0; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--card); + padding: 20px; + box-shadow: 0 18px 50px rgba(15, 23, 42, 0.07); +} + +.opportunity-card-header { + display: flex; + align-items: flex-start; + gap: 12px; + min-width: 0; +} + +.opportunity-card-icon { + display: grid; + width: 34px; + height: 34px; + flex: 0 0 auto; + place-items: center; + border: 1px solid color-mix(in oklab, var(--accent) 24%, var(--border)); + border-radius: 12px; + background: var(--accent-soft); + color: var(--accent); +} + +.opportunity-card-sidebar h3 { + margin: 0; + color: var(--foreground); + font-size: 16px; + font-weight: 820; + letter-spacing: 0; +} + +.opportunity-card-sidebar p { + margin: 4px 0 0; + color: var(--muted-foreground); + font-size: 13px; + line-height: 1.5; +} + +.opp-list { + display: grid; + gap: 8px; +} + +.opp-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + min-height: 42px; + border: 1px solid var(--border); + border-radius: 12px; + background: color-mix(in oklab, var(--muted) 48%, var(--card)); + padding: 8px 12px; +} + +.opp-symbol { + min-width: 0; + overflow: hidden; + color: var(--foreground); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 13px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +.opp-value { + display: inline-flex; + align-items: center; + min-height: 24px; + border-radius: 999px; + padding: 0 9px; + font-size: 11px; + font-weight: 800; + white-space: nowrap; +} + +.opp-value.up { + background: var(--bl-success-muted); + color: var(--bl-success); +} + +.opp-value.down { + background: var(--bl-danger-muted); + color: var(--bl-danger); +} + +.opp-value.ai { + background: var(--bl-info-muted); + color: var(--bl-info); +} + +.opp-empty { + display: grid; + min-height: 120px; + place-items: center; + border: 1px dashed var(--border); + border-radius: 14px; + background: color-mix(in oklab, var(--muted) 42%, transparent); + color: var(--muted-foreground); + padding: 18px; + text-align: center; + font-size: 13px; + line-height: 1.5; +} + .screener-filter-card { overflow: hidden; }