fix(D8): make empty-state chips market-aware

This commit is contained in:
Saravana Achu Mac 2026-05-04 17:56:34 -07:00
parent 909518f82c
commit b1f872f54c
2 changed files with 88 additions and 8 deletions

View File

@ -160,4 +160,27 @@ describe('TickerHeader', () => {
await waitFor(() => expect(screen.getByText('50.0')).toBeInTheDocument());
expect(screen.getAllByText('100.00').length).toBeGreaterThanOrEqual(2);
});
it('uses configured crypto symbols for empty-state suggestions', async () => {
const user = userEvent.setup();
const setActiveSymbol = vi.fn();
const cryptoContext: AppContextValue = {
...appContext,
activeSymbol: '',
setActiveSymbol,
profile: { symbols: 'BTC/USDT, ETH/USDT', market_type: 'crypto' },
botState: {
...appContext.botState,
symbols: {},
} as any,
};
renderTickerHeader(cryptoContext);
expect(screen.getByRole('button', { name: 'BTC/USDT' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'AAPL' })).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'ETH/USDT' }));
expect(setActiveSymbol).toHaveBeenCalledWith('ETH/USDT');
});
});

View File

@ -41,6 +41,41 @@ interface ResearchProfile {
exchangeShortName?: string;
}
const EQUITY_EMPTY_STATE_SYMBOLS = ['AAPL','MSFT','GOOGL','AMZN','NVDA'];
const CRYPTO_EMPTY_STATE_SYMBOLS = ['BTC/USDT','ETH/USDT','SOL/USDT','ADA/USDT','DOGE/USDT'];
const CRYPTO_BASES = new Set(['BTC','ETH','SOL','ADA','DOGE','XRP','LTC','DOT','AVAX','MATIC','BNB']);
function uniqueSymbols(symbols: string[]) {
return Array.from(new Set(symbols.map(s => s.trim().toUpperCase()).filter(Boolean)));
}
function splitSymbols(raw: unknown) {
if (Array.isArray(raw)) return uniqueSymbols(raw.map(String));
if (typeof raw === 'string') return uniqueSymbols(raw.split(','));
return [];
}
function isCryptoLikeSymbol(symbol: string) {
const normalized = symbol.trim().toUpperCase();
const base = normalized.split(/[/-]/)[0];
return normalized.includes('/') || normalized.endsWith('USDT') || CRYPTO_BASES.has(base);
}
function emptyStateSuggestions(profile: any, botSymbols: Record<string, unknown>) {
const configuredSymbols = uniqueSymbols([
...splitSymbols(profile?.symbols),
...Object.keys(botSymbols ?? {}),
]);
if (configuredSymbols.length > 0) return configuredSymbols.slice(0, 5);
const marketHint = String(
profile?.market_type ?? profile?.marketType ?? profile?.asset_class ?? profile?.assetClass ?? profile?.exchange ?? '',
).toLowerCase();
if (marketHint.includes('crypto')) return CRYPTO_EMPTY_STATE_SYMBOLS;
return EQUITY_EMPTY_STATE_SYMBOLS;
}
const INDICATORS: Array<{ key: IndicatorKey; label: string; hint: string }> = [
{ key: 'rsi', label: 'RSI', hint: '14-period momentum' },
{ key: 'macd', label: 'MACD', hint: '12/26 EMA trend' },
@ -751,7 +786,15 @@ function ResearchCards({
}
// ─── Empty state ──────────────────────────────────────────────────────────────
function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
function EmptyState({
onSelect,
suggestions,
}: {
onSelect: (symbol: string) => void;
suggestions: string[];
}) {
const cryptoMode = suggestions.some(isCryptoLikeSymbol);
return (
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
@ -760,28 +803,35 @@ function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
}}>
<div style={{ fontSize: 56 }}>📈</div>
<div style={{ fontSize: 20, fontWeight: 700, color: '#374151' }}>
Search a company to get started
Search an asset to get started
</div>
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
Type a ticker symbol or company name in the search bar above to view charts, financials, and news.
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
</div>
{cryptoMode && (
<div style={{ fontSize: 12, color: '#6B7280', fontWeight: 600 }}>
Suggested from your crypto bot configuration
</div>
)}
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
{['AAPL','MSFT','GOOGL','AMZN','NVDA'].map(t => (
<span
{suggestions.map(t => (
<button
key={t}
onClick={() => onSelect(t)}
style={{
padding: '4px 12px',
background: '#EFF6FF',
color: '#2563EB',
border: '1px solid #BFDBFE',
borderRadius: 20,
fontSize: 13,
fontWeight: 600,
cursor: 'pointer',
fontFamily: 'inherit',
}}
>
{t}
</span>
</button>
))}
</div>
</div>
@ -790,7 +840,7 @@ function EmptyState({ onSelect }: { onSelect: (symbol: string) => void }) {
// ─── HomeView ─────────────────────────────────────────────────────────────────
export function HomeView() {
const { activeSymbol, setActiveSymbol } = useAppContext();
const { activeSymbol, setActiveSymbol, botState, profile: activeProfile } = useAppContext();
const [profile, setProfile] = useState<ResearchProfile | null>(null);
const [profileLoading, setProfileLoading] = useState(false);
const [latestBarTimestamp, setLatestBarTimestamp] = useState<number | null>(null);
@ -819,7 +869,14 @@ export function HomeView() {
return () => { cancelled = true; };
}, [activeSymbol]);
if (!activeSymbol) return <EmptyState onSelect={setActiveSymbol} />;
if (!activeSymbol) {
return (
<EmptyState
onSelect={setActiveSymbol}
suggestions={emptyStateSuggestions(activeProfile, botState.symbols)}
/>
);
}
return (
<div>