feat(ui): one-click 'Hide off-topic' toggle on the Trump feed
Lets the user collapse every post the AI judged off-topic (not crypto-related, never scored) so the feed shows only real signals. - PostCards exports isAiScored(post) as the single source of truth for what counts as noise — the card de-emphasis and the list filter now share it, so they can never disagree. - TrumpPageClient: 'Hide off-topic (N)' toggle in the filter row (only shown when N>0), with live noise count. Resets to page 1 on toggle. tsc + build clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { useLocale } from 'next-intl'
|
|||||||
import type { TrumpPost } from '@/types'
|
import type { TrumpPost } from '@/types'
|
||||||
import { getPosts } from '@/lib/api'
|
import { getPosts } from '@/lib/api'
|
||||||
import { swrFetch } from '@/lib/cache'
|
import { swrFetch } from '@/lib/cache'
|
||||||
import PostRow from '@/components/dashboard/PostCards'
|
import PostRow, { isAiScored } from '@/components/dashboard/PostCards'
|
||||||
import SystemControl from '@/components/signals/SystemControl'
|
import SystemControl from '@/components/signals/SystemControl'
|
||||||
import PageHint from '@/components/ui/PageHint'
|
import PageHint from '@/components/ui/PageHint'
|
||||||
import InfoTip from '@/components/ui/InfoTip'
|
import InfoTip from '@/components/ui/InfoTip'
|
||||||
@@ -29,6 +29,7 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
|
|||||||
const [loadErr, setLoadErr] = useState('')
|
const [loadErr, setLoadErr] = useState('')
|
||||||
const [sentFilter, setSentFilter] = useState<SentimentFilter>('all')
|
const [sentFilter, setSentFilter] = useState<SentimentFilter>('all')
|
||||||
const [sigFilter, setSigFilter] = useState<SignalFilter>('all')
|
const [sigFilter, setSigFilter] = useState<SignalFilter>('all')
|
||||||
|
const [hideNoise, setHideNoise] = useState(false)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,13 +50,19 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
|
|||||||
[posts],
|
[posts],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const noiseCount = useMemo(
|
||||||
|
() => trumpPosts.filter(p => !isAiScored(p)).length,
|
||||||
|
[trumpPosts],
|
||||||
|
)
|
||||||
|
|
||||||
const filtered = useMemo(() => trumpPosts.filter(p => {
|
const filtered = useMemo(() => trumpPosts.filter(p => {
|
||||||
|
if (hideNoise && !isAiScored(p)) return false
|
||||||
if (sentFilter !== 'all' && p.sentiment !== sentFilter) return false
|
if (sentFilter !== 'all' && p.sentiment !== sentFilter) return false
|
||||||
if (sigFilter === 'actionable' && p.signal !== 'buy' && p.signal !== 'short') return false
|
if (sigFilter === 'actionable' && p.signal !== 'buy' && p.signal !== 'short') return false
|
||||||
if (sigFilter === 'buy' && p.signal !== 'buy') return false
|
if (sigFilter === 'buy' && p.signal !== 'buy') return false
|
||||||
if (sigFilter === 'short' && p.signal !== 'short') return false
|
if (sigFilter === 'short' && p.signal !== 'short') return false
|
||||||
return true
|
return true
|
||||||
}), [trumpPosts, sentFilter, sigFilter])
|
}), [trumpPosts, sentFilter, sigFilter, hideNoise])
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
||||||
const safePage = Math.min(page, totalPages)
|
const safePage = Math.min(page, totalPages)
|
||||||
@@ -116,6 +123,23 @@ export default function TrumpSignalPage({ initialPosts = null }: TrumpSignalPage
|
|||||||
|
|
||||||
{/* Sentiment filter */}
|
{/* Sentiment filter */}
|
||||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
{/* One-click collapse of off-topic (non-crypto, un-scored) posts. */}
|
||||||
|
{noiseCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setHideNoise(v => !v); setPage(1) }}
|
||||||
|
title="Collapse posts the AI judged off-topic (not crypto-related)"
|
||||||
|
style={{
|
||||||
|
padding: '4px 10px', borderRadius: 6,
|
||||||
|
border: '1px solid var(--line)',
|
||||||
|
background: hideNoise ? 'var(--ink)' : 'transparent',
|
||||||
|
color: hideNoise ? 'var(--bg)' : 'var(--ink-3)',
|
||||||
|
fontSize: 11, cursor: 'pointer', fontWeight: 600,
|
||||||
|
marginRight: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hideNoise ? `✓ Off-topic hidden (${noiseCount})` : `🙈 Hide off-topic (${noiseCount})`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<span style={{ fontSize: 11, color: 'var(--ink-4)', marginRight: 4 }}>
|
<span style={{ fontSize: 11, color: 'var(--ink-4)', marginRight: 4 }}>
|
||||||
Sentiment
|
Sentiment
|
||||||
<InfoTip
|
<InfoTip
|
||||||
|
|||||||
@@ -3,6 +3,16 @@
|
|||||||
import { memo, useEffect, useState } from 'react'
|
import { memo, useEffect, useState } from 'react'
|
||||||
import type { TrumpPost } from '@/types'
|
import type { TrumpPost } from '@/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was this post actually AI-scored, or filtered as off-topic noise before any
|
||||||
|
* AI call? Single source of truth — used both by the card (to de-emphasise
|
||||||
|
* noise) and by list views (to offer a "collapse off-topic" toggle), so the
|
||||||
|
* two can never disagree about what counts as noise.
|
||||||
|
*/
|
||||||
|
export function isAiScored(post: Pick<TrumpPost, 'ai_confidence' | 'ai_reasoning'>): boolean {
|
||||||
|
return (post.ai_confidence ?? 0) > 0 || !!post.ai_reasoning
|
||||||
|
}
|
||||||
|
|
||||||
function fmtPct(n: number | null | undefined) {
|
function fmtPct(n: number | null | undefined) {
|
||||||
if (n == null || isNaN(n)) return '—' // null = window not yet closed
|
if (n == null || isNaN(n)) return '—' // null = window not yet closed
|
||||||
const s = n.toFixed(2) + '%'
|
const s = n.toFixed(2) + '%'
|
||||||
@@ -102,14 +112,10 @@ const PostRow = memo(function PostRow({ post, selected, onClick }: PostRowProps)
|
|||||||
onClick?.()
|
onClick?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Did this post actually get an AI score, or was it filtered as off-topic
|
// Off-topic Trump posts (most of them) are filtered before any AI call, so
|
||||||
// noise before any AI call? Trump posts mostly aren't crypto-related, so the
|
// they carry confidence 0 + no reasoning. Treat those as un-scored noise and
|
||||||
// entry filter / AI marks them relevant=false with confidence 0 and no
|
// de-emphasise them. Shared helper so list-level "collapse off-topic" agrees.
|
||||||
// reasoning. Showing "AI confidence 0%" + an empty bar for those is
|
const aiScored = isAiScored(post)
|
||||||
// misleading — it reads as "AI looked and had zero confidence" when really
|
|
||||||
// "this was skipped as not crypto-relevant". Treat a post as AI-scored only
|
|
||||||
// when it has a real confidence or reasoning.
|
|
||||||
const aiScored = post.ai_confidence > 0 || !!post.ai_reasoning
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user