KOL count: 29 → 25 across marketing/SEO copy

Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists

Bundles other in-flight frontend work already in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
k
2026-06-09 22:55:27 +08:00
parent 9e0f6554cb
commit 4c3c8c6f87
57 changed files with 3464 additions and 1855 deletions
+239 -86
View File
@@ -9,12 +9,14 @@ import { useAccount } from 'wagmi'
import type { TrumpPost, BotPerformance, Candle } from '@/types'
import { useDashboardStore } from '@/store/dashboard'
import { usePriceSocket } from '@/lib/useRealtimeData'
import { getMacroSnapshot, getPerformance, getPrices, getUserPublic, type MacroSnapshot } from '@/lib/api'
import { getMacroSnapshot, getPerformance, getPrices, getUserPublic, getKolDivergence, getKolDigest, type MacroSnapshot } from '@/lib/api'
import type { KolDivergence, KolDigest } from '@/types'
import { getCachedViewEnvelope } from '@/lib/signedRequest'
import { swrFetch } from '@/lib/cache'
import PostRow, { SignalPill, SourceIcon, fmtPct, TimeAgo, LocalDateTime } from '@/components/dashboard/PostCards'
import { swrFetch, invalidate as invalidateCache } from '@/lib/cache'
import PostRow, { SignalPill, SourceIcon, SOURCE_DISPLAY, fmtPct, TimeAgo, LocalDateTime } from '@/components/dashboard/PostCards'
import OpenPositions from '@/components/positions/OpenPositions'
import PageHint from '@/components/ui/PageHint'
import AnimatedNumber from '@/components/ui/AnimatedNumber'
// Heavy components — lazy-loaded so they don't bloat the initial JS bundle.
// ChartPanel pulls in lightweight-charts (~200KB gz); split it out.
@@ -58,7 +60,9 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void })
<div className="row gap-s" style={{ marginBottom: 12 }}>
<SourceIcon source={post.source} />
<div>
<div style={{ fontSize: 13, fontWeight: 500, fontFamily: 'var(--mono)' }}>@realDonaldTrump</div>
<div style={{ fontSize: 13, fontWeight: 500, fontFamily: 'var(--mono)' }}>
{SOURCE_DISPLAY[post.source?.toLowerCase?.()]?.label ?? post.source ?? 'Signal'}
</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)' }}><TimeAgo iso={post.published_at} suffix=" ago" /> · <LocalDateTime iso={post.published_at} /></div>
</div>
</div>
@@ -86,7 +90,9 @@ function PostDetail({ post, onClose }: { post: TrumpPost; onClose: () => void })
{/* AI reasoning */}
{post.ai_reasoning && (
<>
<div className="ai-reasoning-label">AI reasoning</div>
<div className="ai-reasoning-label">
{post.signal === 'buy' || post.signal === 'short' ? 'Why this signal fired' : 'Why this was filtered out'}
</div>
<div className="ai-reasoning-card scroll" style={{ marginBottom: 16 }}>
{post.ai_reasoning}
</div>
@@ -147,7 +153,7 @@ function SelectHint() {
export default function DashboardClient({ initialPosts }: Props) {
const intlLocale = useLocale()
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness, livePrices } = useDashboardStore()
const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, setPaperMode, isSubscribed, hlApiKeySet, botReadiness, livePrices } = useDashboardStore()
function setTimeframe(tf: string) { _setTimeframe(tf as '5m' | '15m' | '1H' | '4H' | '1D' | '1W') }
const { address, isConnected } = useAccount()
const params = useParams()
@@ -159,29 +165,40 @@ export default function DashboardClient({ initialPosts }: Props) {
const [chartReload, setChartReload] = useState(0)
const [selectedPostId, setSelectedPostId] = useState<number | null>(null)
const [macro, setMacro] = useState<MacroSnapshot | null>(null)
const [kolDivergences, setKolDivergences] = useState<KolDivergence[]>([])
const [kolDigest, setKolDigest] = useState<KolDigest | null>(null)
// For 1D: show all posts from selected day
const [selectedDayPosts, setSelectedDayPosts] = useState<TrumpPost[] | null>(null)
useEffect(() => {
// B42: cancel the in-flight getUserPublic if the wallet switches again
// before it resolves — prevents old wallet's data polluting new wallet's state.
let cancelled = false
if (!isConnected || !address) {
setSubscribed(false)
setHlApiKeySet(false)
setBotReadiness('unknown')
setPerformance(undefined)
return
return () => { cancelled = true }
}
// Clear account-scoped bot state immediately when the connected wallet
// changes so a previous wallet's status never leaks into the next one.
// Clear account-scoped bot state immediately (setWallet already resets these
// via store, but DashboardClient may be rendered without a wallet switch —
// keep explicit resets here for clarity and safety).
setSubscribed(false)
setHlApiKeySet(false)
setBotReadiness('unknown')
// `snapAddr !== address` would be a stale-closure trap: both are the same
// closed-over value. Use `cancelled` (set by effect cleanup) as the sole guard.
getUserPublic(address.toLowerCase())
.then((user) => {
if (cancelled) return // effect cleaned up = wallet changed or unmounted
setSubscribed(user.active)
setHlApiKeySet(user.hl_api_key_set)
setBotReadiness(user.hl_api_key_set ? 'saved' : 'unknown')
setPaperMode(!!user.paper_mode)
})
.catch(() => {})
return () => { cancelled = true }
}, [address, isConnected, setSubscribed, setHlApiKeySet, setBotReadiness])
useEffect(() => {
@@ -207,19 +224,37 @@ export default function DashboardClient({ initialPosts }: Props) {
return () => { cancelled = true }
}, [address, isConnected])
const [freshPostId, setFreshPostId] = useState<number | null>(null)
usePriceSocket({
onPrice: (a, price) => setLivePrice(a, price),
onNewPost: (post) => setPosts((prev) => [post as TrumpPost, ...prev].slice(0, 500)),
onNewPost: (post) => {
const p = post as TrumpPost
// Dedup: WS may resend a post already in initialPosts or a prior push.
setPosts((prev) => prev.some(x => x.id === p.id) ? prev : [p, ...prev].slice(0, 500))
setFreshPostId(p.id)
// Keep a ref to the timer so we can cancel it if the component unmounts
// before it fires (avoids setState-after-unmount warning).
const timer = setTimeout(() => setFreshPostId((id) => (id === p.id ? null : id)), 1400)
return () => clearTimeout(timer) // returned but not used by usePriceSocket — see L1 note
},
})
useEffect(() => {
let cancelled = false
setCandles([])
setChartErr('')
getPrices(asset, timeframe)
.then(c => { setCandles(c); setChartErr('') })
.catch(e => setChartErr(e instanceof Error ? e.message : (isZh ? '价格数据加载失败' : 'Failed to load price data')))
// isZh intentionally excluded: it is a compile-time constant (always false)
// and including it would restart the chart fetch on every render.
const priceKey = `prices-${asset}-${timeframe}`
if (chartReload > 0) invalidateCache(priceKey)
swrFetch(
priceKey,
90_000,
() => getPrices(asset, timeframe),
fresh => { if (!cancelled) { setCandles(fresh); setChartErr('') } },
)
.then(c => { if (!cancelled) { setCandles(c); setChartErr('') } })
.catch(e => { if (!cancelled) setChartErr(e instanceof Error ? e.message : 'Failed to load price data') })
return () => { cancelled = true }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [asset, timeframe, chartReload])
@@ -240,6 +275,21 @@ export default function DashboardClient({ initialPosts }: Props) {
return () => { alive = false; clearInterval(id) }
}, [])
// KOL divergence + digest — loaded once, 30 min cache (changes daily)
useEffect(() => {
let alive = true
// KOL ingestion is daily and sparse — a 7d window is frequently empty
// (e.g. 7d=0 while 30d=196), which hid the whole sidebar card. Use a 30d
// window so the Overview reflects the data that actually exists.
swrFetch('kol-divergence-30d', 30 * 60_000, () => getKolDivergence({ days: 30 }), f => { if (alive) setKolDivergences(f.items ?? []) })
.then(r => { if (alive) setKolDivergences(r.items ?? []) })
.catch(() => {})
swrFetch('kol-digest-30d', 30 * 60_000, () => getKolDigest(30), f => { if (alive) setKolDigest(f) })
.then(r => { if (alive) setKolDigest(r) })
.catch(() => {})
return () => { alive = false }
}, [])
const selectedPost = posts.find(p => p.id === selectedPostId) ?? null
// Show actionable signals first (buy/short), then most recent hold/neutral.
@@ -278,7 +328,9 @@ export default function DashboardClient({ initialPosts }: Props) {
const actionablePosts = posts.filter(p => p.signal === 'buy' || p.signal === 'short').length
const trumpActionable = posts.filter(p => (p.source || '') === 'truth' && (p.signal === 'buy' || p.signal === 'short')).length
const macroActionable = posts.filter(p => ((p.source || '') === 'btc_bottom_reversal' || (p.source || '') === 'funding_reversal') && (p.signal === 'buy' || p.signal === 'short')).length
const kolMentions = posts.filter(p => (p.source || '') === 'kol').length
// Use actual KOL divergence count (divergence-flagged items from the API),
// not a source==='kol' post count (which never matches — source is 'substack'/'blog'/etc.).
const kolMentions = kolDivergences.filter(d => d.signal_type === 'divergence').length
const winRate = performance?.win_rate ?? 0
const netPnl = performance?.net_pnl_usd ?? 0
const hasPriceData = candles.length > 0
@@ -286,37 +338,38 @@ export default function DashboardClient({ initialPosts }: Props) {
const macroScore = macro?.composite_score ?? null
const macroRegime = macro?.regime_label ?? null
const macroPct = macroScore == null ? 50 : Math.max(0, Math.min(100, (macroScore + 100) / 2))
// Derive tone from the backend's regime_label, NOT a re-thresholded score.
// The backend uses ±20 for BULLISH/BEARISH; re-deriving with ±15 here made
// a score of e.g. 15.2 read "Supportive" in the card while the same card's
// regime label said NEUTRAL. Trusting regime_label keeps them consistent.
const macroTone =
macroScore == null ? 'neutral'
: macroScore > 15 ? 'bull'
: macroScore < -15 ? 'bear'
macroRegime == null ? (macroScore == null ? 'neutral' : 'neutral')
: macroRegime === 'BULL' || macroRegime === 'BULLISH' ? 'bull'
: macroRegime === 'BEAR' || macroRegime === 'BEARISH' ? 'bear'
: 'neutral'
const macroSummary =
macroScore == null ? 'Daily macro composite not loaded yet.'
: macroTone === 'bull' ? 'Risk backdrop is supportive. Trend-following setups get more room.'
: macroTone === 'bear' ? 'Backdrop is defensive. Preserve size and expect cleaner downside moves.'
: 'Backdrop is mixed. Useful for context, not a blind directional trigger.'
: macroTone === 'bull' ? 'Supportive backdrop — trend setups have room.'
: macroTone === 'bear' ? 'Defensive backdrop — preserve size, downside moves are cleaner.'
: 'Mixed backdrop context only, not a directional trigger.'
return (
<div className="page wide">
<div className="page-head">
<div>
<h1 className="page-title">{isZh ? '信号总览' : 'Signal monitor'}</h1>
<PageHint count={`${actionablePosts} actionable · ${totalPosts} tracked`}>
Four signals that move crypto before the crowd sees them Trump
posts, BTC macro bottoms, funding extremes, and what KOLs do vs
say. Tracked live, in public, every call timestamped.
<PageHint count={actionablePosts > 0 ? `${actionablePosts} actionable · ${totalPosts} tracked` : undefined}>
Trump · Macro · KOL divergence live.
</PageHint>
</div>
<div className="row gap-s">
<span className="chip"><span className="live-dot" />Live feed</span>
</div>
<span className="chip"><span className="live-dot" />Live</span>
</div>
{/* Open positions — what's on the book right now. Renders only when
a subscribed wallet is connected, so guests see the normal feed. */}
<OpenPositions />
<div className="overview-shell">
<div className="overview-main">
<section className="overview-market-card">
@@ -324,12 +377,15 @@ export default function DashboardClient({ initialPosts }: Props) {
<div>
<div className="overview-kicker">Market and macro</div>
<div className="overview-headline-row">
<div className="hero-value mono" style={{ fontSize: 40 }}>
{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
</div>
<AnimatedNumber
className="hero-value mono"
style={{ fontSize: 40 }}
value={displayPrice}
display={displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
/>
<span className={`chip ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · 24h</span>
</div>
<div className="overview-market-subtitle">BTC spot with live signal context</div>
<div className="overview-market-subtitle">{asset} · live signal context</div>
</div>
<div className="overview-controls">
<div className="asset-switch">
@@ -374,62 +430,134 @@ export default function DashboardClient({ initialPosts }: Props) {
</div>
</div>
<div className="overview-system-strip">
<Link href={`/${locale}/trump`} className="overview-system-chip">
<span className="overview-system-chip-name">Trump</span>
<strong>{trumpActionable}</strong>
</Link>
<Link href={`/${locale}/macro`} className="overview-system-chip">
<span className="overview-system-chip-name">Macro</span>
<strong>{macroActionable}</strong>
</Link>
<Link href={`/${locale}/kol`} className="overview-system-chip">
<span className="overview-system-chip-name">KOL</span>
<strong>{kolMentions}</strong>
</Link>
<div className="overview-system-chip passive">
<span className="overview-system-chip-name">Signals today</span>
<strong>{signalsToday}</strong>
</div>
</div>
</section>
<div className="overview-secondary-grid">
<section className="overview-stat-card">
<div className="overview-kicker">Execution</div>
<div className="overview-stat-value">{actionablePosts}</div>
<div className="overview-stat-label">actionable signals tracked in feed</div>
</section>
<section className="overview-stat-card accent">
<div className="overview-kicker">Performance</div>
<div className="overview-stat-value">{hasPerformanceData ? `${netPnl >= 0 ? '+$' : '-$'}${Math.abs(netPnl).toLocaleString('en-US', { maximumFractionDigits: 0 })}` : '—'}</div>
<div className="overview-stat-label">{hasPerformanceData ? '30d live net P&L (real trades only)' : 'Load settings once to unlock private performance'}</div>
</section>
{/* ── Unified stats row ─────────────────────────────────────────── */}
<div className="overview-stats-bar">
{([
{ label: 'Trump signals', value: trumpActionable, href: `/${locale}/trump` },
{ label: 'Macro signals', value: macroActionable, href: `/${locale}/macro` },
{ label: 'KOL divergence', value: kolMentions, href: `/${locale}/kol` },
] as const).map(({ label, value, href }) => {
const empty = value === 0
const inner = (
<>
<span className="stats-bar-label">{label}</span>
<span className="stats-bar-value" style={{ color: empty ? 'var(--ink-4)' : 'var(--ink)' }}>
{empty ? '—' : value}
</span>
</>
)
return <Link key={label} href={href} className="stats-bar-item">{inner}</Link>
})}
<div className="stats-bar-item passive" style={{ borderRight: 'none' }}>
<span className="stats-bar-label">My P&amp;L · 30d</span>
<span className="stats-bar-value" style={{
color: hasPerformanceData ? (netPnl >= 0 ? 'var(--up)' : 'var(--down)') : 'var(--ink-4)',
}}>
{hasPerformanceData
? `${netPnl >= 0 ? '+' : ''}$${Math.abs(netPnl).toLocaleString('en-US', { maximumFractionDigits: 0 })}`
: '—'}
</span>
</div>
</div>
</div>
<aside className="overview-side">
<section className="overview-side-card">
<div className="overview-kicker">Account</div>
<div className="overview-account-list">
<div className="overview-account-item">
<span>Wallet</span>
<strong>{isConnected && address ? `${address.slice(0, 6)}${address.slice(-4)}` : 'Not connected'}</strong>
{isConnected ? (
<section className="overview-side-card">
<div className="overview-kicker">Account</div>
<div className="overview-account-list">
<div className="overview-account-item">
<span>Wallet</span>
<strong className="mono">{address ? `${address.slice(0, 6)}${address.slice(-4)}` : '—'}</strong>
</div>
<div className="overview-account-item">
<span>Settings</span>
<strong>{hasPerformanceData ? 'Loaded' : 'Not loaded'}</strong>
</div>
{hasPerformanceData && (
<div className="overview-account-item">
<span>Win rate</span>
<strong>{`${(winRate * 100).toFixed(1)}%`}</strong>
</div>
)}
</div>
<div className="overview-account-item">
<span>Private data</span>
<strong>{hasPerformanceData ? 'Unlocked' : 'Locked until Settings load'}</strong>
</section>
) : (
<section className="overview-side-card" style={{ padding: '14px 16px' }}>
<div className="overview-kicker" style={{ marginBottom: 12 }}>Get started</div>
{([
{ icon: '📡', head: 'Free signals', sub: 'Trump · Macro · KOL — no account needed' },
{ icon: '🔔', head: 'Telegram alerts', sub: 'Signal fires → your phone in seconds' },
{ icon: '⚡', head: 'Auto-trade', sub: 'Trade-only key · no withdrawal access' },
] as const).map(({ icon, head, sub }) => (
<div key={head} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginBottom: 12 }}>
<span style={{ fontSize: 16, flexShrink: 0, marginTop: 1 }}>{icon}</span>
<div>
<div style={{ fontSize: 12, fontWeight: 700, lineHeight: 1.3 }}>{head}</div>
<div style={{ fontSize: 11, color: 'var(--ink-4)', lineHeight: 1.4, marginTop: 1 }}>{sub}</div>
</div>
</div>
))}
</section>
)}
{/* KOL divergence hook — highest-conviction signal type */}
{(kolDivergences.length > 0 || (kolDigest && kolDigest.tickers.length > 0)) && (
<section className="overview-side-card" style={{ padding: '14px 16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<div className="overview-kicker">KOL intel · 30d</div>
<a href={`/${locale}/kol`} style={{ fontSize: 10, color: 'var(--amber-ink)', textDecoration: 'none', fontWeight: 700 }}>
Full feed
</a>
</div>
<div className="overview-account-item">
<span>Win rate</span>
<strong>{hasPerformanceData ? `${(winRate * 100).toFixed(1)}%` : '—'}</strong>
</div>
</div>
</section>
<section className="overview-side-card compact">
<div className="overview-kicker">Chart focus</div>
<div className="overview-side-copy">Live chart with signal markers and drill-down on click.</div>
</section>
{/* Latest divergence — the money signal */}
{kolDivergences.filter(d => d.signal_type === 'divergence').slice(0, 1).map(d => (
<div key={d.id} style={{
padding: '9px 11px', borderRadius: 7, marginBottom: 8,
background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.25)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
<span style={{ fontSize: 10, fontWeight: 800, color: '#f59e0b', letterSpacing: '0.05em' }}> DIVERGENCE</span>
<span style={{ fontSize: 10, color: 'var(--ink-4)' }}>
{/* post_at = when the KOL actually published. created_at
is the DB write time, which a backfill/late scan can
set to "now", making an old event look fresh. */}
{Math.round((Date.now() - new Date(d.post_at).getTime()) / 864e5)}d ago
</span>
</div>
<div style={{ fontSize: 12, fontWeight: 600 }}>
@{d.handle} said <span style={{ color: d.post_action === 'bullish' || d.post_action === 'buy' ? 'var(--up)' : 'var(--down)' }}>{d.post_action}</span>
{' '}on <strong>{d.ticker}</strong>
</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>
Then {d.onchain_action.replace('_', ' ')}{d.usd_after != null ? `$${(d.usd_after / 1000).toFixed(0)}k on-chain` : ''}
</div>
</div>
))}
{/* Top digest tickers */}
{kolDigest && kolDigest.tickers.slice(0, 2).map(t => (
<div key={t.ticker} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '6px 0', borderBottom: '1px solid var(--line)',
}}>
<div>
<span style={{ fontSize: 12, fontWeight: 700 }}>{t.ticker}</span>
<span style={{ fontSize: 10, color: 'var(--ink-4)', marginLeft: 6 }}>{t.kol_count} KOLs</span>
</div>
<span style={{
fontSize: 10, fontWeight: 700, padding: '2px 7px', borderRadius: 4,
background: t.side === 'long' ? 'var(--up-soft)' : t.side === 'short' ? 'rgba(220,38,38,.12)' : 'var(--bg-sunk)',
color: t.side === 'long' ? 'var(--up)' : t.side === 'short' ? 'var(--down)' : 'var(--ink-3)',
}}>
{t.dominant_action.toUpperCase()}
</span>
</div>
))}
</section>
)}
</aside>
</div>
@@ -441,9 +569,12 @@ export default function DashboardClient({ initialPosts }: Props) {
<div>
<div className="tiny">Price · {asset}</div>
<div className="row gap-m" style={{ marginTop: 6 }}>
<div className="hero-value mono" style={{ fontSize: 32 }}>
{displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
</div>
<AnimatedNumber
className="hero-value mono"
style={{ fontSize: 32 }}
value={displayPrice}
display={displayPrice != null ? '$' + Math.round(displayPrice).toLocaleString() : '—'}
/>
<span className={`chip ${priceChange >= 0 ? 'up' : 'down'}`}>{hasPriceData ? fmtPct(priceChange) : 'Feed pending'} · 24h</span>
</div>
</div>
@@ -459,7 +590,20 @@ export default function DashboardClient({ initialPosts }: Props) {
onClick={() => setChartReload(n => n + 1)}>Retry</button>
</div>
)}
<div className="chart-wrap">
<div className="chart-wrap" style={{ position: 'relative' }}>
{/* Loading overlay — shown while candles are fetching (candles=[] and no error).
Prevents the user from seeing a blank lightweight-charts canvas. */}
{!hasPriceData && !chartErr && (
<div style={{
position: 'absolute', inset: 0, zIndex: 4,
background: 'var(--bg-sunk)', borderRadius: 'var(--r-sm)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
gap: 10, color: 'var(--ink-3)', fontSize: 13,
}}>
<span className="live-dot" style={{ background: 'var(--amber)' }} />
Loading chart
</div>
)}
<ChartPanel
posts={posts}
candles={candles}
@@ -495,7 +639,16 @@ export default function DashboardClient({ initialPosts }: Props) {
</div>
<div className="post-stream">
{recentPosts.map(p => (
<PostRow key={p.id} post={p} />
<div key={p.id} className={p.id === freshPostId ? 'signal-enter' : undefined}>
<PostRow post={p} selected={selectedPostId === p.id} onClick={() => {
// Clear the day-list view first: the right panel renders
// selectedDayPosts with higher priority than selectedPost,
// so without this the detail never appears while a candle's
// multi-post list is open.
setSelectedDayPosts(null)
setSelectedPostId(selectedPostId === p.id ? null : p.id)
}} />
</div>
))}
</div>
</div>