diff --git a/backend/routers/papers.py b/backend/routers/papers.py index 496bcc86..4630eb29 100644 --- a/backend/routers/papers.py +++ b/backend/routers/papers.py @@ -526,51 +526,6 @@ def search_papers_feed( ) -@router.get( - "/papers/x-trending", - response_model=Papers, - summary="Papers trending on X, ordered by trending score", -) -def get_x_trending_papers( - page: int = Query(1, ge=1, le=100, description="Page number (1-based)"), - page_size: int = Query(30, ge=1, le=100, description="Papers per page"), - start_date: Optional[date] = Query( - None, description="Inclusive earliest publication date (YYYY-MM-DD)" - ), - end_date: Optional[date] = Query( - None, description="Inclusive latest publication date (YYYY-MM-DD)" - ), - include_resources: bool = Query( - False, description="Include repositories and project pages" - ), - has_code: bool = Query( - False, description="Only include papers with a linked GitHub repository" - ), - has_hf_artifacts: bool = Query( - False, description="Only include papers with linked Hugging Face artifacts" - ), - db: Session = Depends(get_database_session), -): - """List papers from the daily "AI Papers on X" picks. - - Like the homepage trending feed, the date window filters on publication - date and papers are ordered by the same trending score. - """ - if start_date is not None and end_date is not None and start_date > end_date: - raise HTTPException( - status_code=422, detail="start_date must be on or before end_date" - ) - return XTrendingService(db).feed( - page=page, - page_size=page_size, - start_date=start_date, - end_date=end_date, - include_resources=include_resources, - has_code=has_code, - has_hf_artifacts=has_hf_artifacts, - ) - - @router.put( "/papers/x-trending/{shared_on}", response_model=XTrendingDayResult, diff --git a/backend/services/x_trending_service.py b/backend/services/x_trending_service.py index 15a4dea5..c86a1d36 100644 --- a/backend/services/x_trending_service.py +++ b/backend/services/x_trending_service.py @@ -1,4 +1,4 @@ -"""Papers trending on X: the daily "AI Papers on X" sheet and its homepage feed. +"""Papers trending on X: the daily "AI Papers on X" sheet. A headless routine exports each dated sheet tab and replaces that day's rows through the admin API. ArXiv papers missing from the catalog are imported; @@ -8,22 +8,18 @@ from datetime import date -from sqlalchemy import or_, select from sqlalchemy.orm import Session from backend.models.database import Paper as DBPaper, PaperXTrending -from backend.models.papers import Papers from backend.models.x_trending import ( XTrendingDayResult, XTrendingPaperInput, XTrendingUnresolved, ) -from backend.services.paper_feed_executor import GENERAL_ORDERING, PaperFeedExecutor from backend.services.paper_ingestion_service import ( PaperIngestionError, PaperIngestionService, ) -from backend.services.paper_service import PaperService class XTrendingService: @@ -100,41 +96,3 @@ async def _resolve( .first() ) return paper, "" if paper else "no external paper with this source URL" - - def feed( - self, - *, - page: int, - page_size: int, - start_date: date | None, - end_date: date | None, - include_resources: bool, - has_code: bool, - has_hf_artifacts: bool, - ) -> Papers: - """Papers ever shared on X, published within the window, by trending score. - - Like the other homepage tabs, the window filters on publication date. - """ - shared = select(PaperXTrending.paper_id) - # Match the stored paper's arXiv id too, so a newer version that has - # since become the latest row stays in the feed. - shared_arxiv_ids = select(DBPaper.arxiv_id).where( - DBPaper.id.in_(shared), DBPaper.arxiv_id.is_not(None) - ) - papers = PaperService(self.db) - return PaperFeedExecutor(papers).execute( - self.db.query(DBPaper).filter( - or_(DBPaper.id.in_(shared), DBPaper.arxiv_id.in_(shared_arxiv_ids)) - ), - page=page, - page_size=page_size, - order_by="trending", - order_dir="desc", - ordering=GENERAL_ORDERING, - has_code=has_code, - has_hf_artifacts=has_hf_artifacts, - start_date=start_date, - end_date=end_date, - include_resources=include_resources, - ) diff --git a/backend/tests/paper/test_x_trending.py b/backend/tests/paper/test_x_trending.py index 69d5ea98..7bb1e20a 100644 --- a/backend/tests/paper/test_x_trending.py +++ b/backend/tests/paper/test_x_trending.py @@ -18,10 +18,8 @@ PaperXTrending, Repository, ) -from backend.models.papers import Paper as PaperCard from backend.models.x_trending import XTrendingPaperInput from backend.services.paper_ingestion_service import PaperIngestionError -from backend.services.paper_service import PaperService from backend.services.x_trending_service import XTrendingService @@ -48,7 +46,7 @@ def paper(ident, *, score=None, arxiv_id=None, source="arxiv", source_url=None): @pytest.fixture -def db(monkeypatch): +def db(): engine = create_engine("sqlite://") Base.metadata.create_all( engine, @@ -64,17 +62,7 @@ def db(monkeypatch): ) ], ) - monkeypatch.setattr( - PaperService, - "hydrate_cards", - lambda self, rows, **_: [ - PaperCard.model_construct(id=str(row.id)) for row in rows - ], - ) with Session(engine) as session: - monkeypatch.setattr( - PaperService(session).settings, "trending_ranking_mode", "composite" - ) session.add_all( [ paper(1, score=5.0, arxiv_id="2610.00001"), @@ -117,19 +105,6 @@ def replace(db, day, rows): return asyncio.run(XTrendingService(db, FakeIngestion(db)).replace_day(day, rows)) -def feed(db, **window): - response = XTrendingService(db).feed( - page=1, - page_size=10, - start_date=window.get("start_date"), - end_date=window.get("end_date"), - include_resources=False, - has_code=False, - has_hf_artifacts=False, - ) - return [card.id for card in response.results] - - def test_replace_day_imports_matches_and_reports_unresolved(db): result = replace( db, @@ -157,38 +132,6 @@ def test_replace_day_imports_matches_and_reports_unresolved(db): assert [entry.paper_id for entry in db.query(PaperXTrending).all()] == [2] -def test_feed_orders_by_trending_score_within_publication_window(db): - replace( - db, - date(2026, 10, 29), - [row(1, arxiv_id="2610.00001"), row(2, arxiv_id="2610.00003")], - ) - replace( - db, - date(2026, 10, 30), - [row(1, arxiv_id="2610.00002"), row(2, arxiv_id="2610.00004")], - ) - # Paper 4 has the top score and was shared this week, but was published - # in August, outside the window. - db.get(Paper, 4).date_published = date(2026, 8, 1) - db.commit() - - window = {"start_date": date(2026, 8, 25), "end_date": date(2026, 9, 1)} - # Unscored paper 3 stays listed after the scored ones. - assert feed(db, **window) == ["2", "1", "3"] - assert feed(db) == ["4", "2", "1", "3"] - - def test_input_needs_an_identifier(): with pytest.raises(ValueError): XTrendingPaperInput(rank=1, title="No link") - - -def test_feed_follows_a_newer_version_of_a_shared_paper(db): - replace(db, date(2026, 10, 30), [row(1, arxiv_id="2610.00001")]) - db.get(Paper, 1).is_latest_version = False - newer = paper(7, score=5.0, arxiv_id="2610.00001") - newer.version = "v2" - db.add(newer) - db.commit() - assert feed(db) == ["7"] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fbd20133..556e7135 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -285,7 +285,8 @@ const Shell: React.FC = () => { }> } /> } /> - } /> + {/* The trending (X) tab merged into the homepage feed. */} + } /> {/* Keep new and persisted chats on the same route instance. A new turn replaces /chat with /chat/:sessionId as soon as the server allocates it; sibling routes would unmount ChatView and abort diff --git a/frontend/src/lib/api/papers.ts b/frontend/src/lib/api/papers.ts index c803b4c8..f53cd77c 100644 --- a/frontend/src/lib/api/papers.ts +++ b/frontend/src/lib/api/papers.ts @@ -173,11 +173,6 @@ export class PapersApi { }); } - /** Papers from the daily "AI Papers on X" picks; dates filter publication, as on other feeds. */ - xTrending(params?: Pick): Promise> { - return this.request(`/papers/x-trending${query(params)}`); - } - forTask( taskId: string, params?: PaperFeedParams & { repository_limit?: number }, diff --git a/frontend/src/lib/paperQueryKeys.ts b/frontend/src/lib/paperQueryKeys.ts index 7accc003..e70b5d55 100644 --- a/frontend/src/lib/paperQueryKeys.ts +++ b/frontend/src/lib/paperQueryKeys.ts @@ -7,7 +7,6 @@ export const paperQueryKeys = { feeds: { list: ["papers"], infinite: ["papers-infinite"], - xTrending: ["x-trending-papers-infinite"], following: ["research-feed"], search: ["paper-search-feed"], task: ["task-papers-infinite"], diff --git a/frontend/src/views/FeedView.tsx b/frontend/src/views/FeedView.tsx index 81311555..a4d02969 100644 --- a/frontend/src/views/FeedView.tsx +++ b/frontend/src/views/FeedView.tsx @@ -307,7 +307,7 @@ const FOLLOW_KIND_LABELS: Record = { }; const FOLLOW_KIND_ORDER = Object.keys(FOLLOW_KIND_LABELS) as FollowKind[]; -type FeedMode = "trending" | "x" | "following"; +type FeedMode = "trending" | "following"; const FEED_PAGE_SIZE = 30; const FOLLOWING_PAGE_SIZE = 40; @@ -373,7 +373,7 @@ export const HomeFeed: React.FC<{ mode: FeedMode }> = ({ mode }) => { const infinite = usePaperFeedQuery( following ? [...paperQueryKeys.feeds.following, user?.hf_subject, followingFilters] - : [...(mode === "x" ? paperQueryKeys.feeds.xTrending : paperQueryKeys.feeds.trending), dateFilters], + : [...paperQueryKeys.feeds.trending, dateFilters], async (page) => { if (following) { const entries = await researchApi.following(page, followingFilters); @@ -381,9 +381,7 @@ export const HomeFeed: React.FC<{ mode: FeedMode }> = ({ mode }) => { return { results: entries, next_page }; } const params = { ...dateFilters, page, page_size: FEED_PAGE_SIZE, include_resources: true }; - return toFeedPage(mode === "x" - ? papersApi.xTrending(params) - : papersApi.list({ ...params, order_by: "trending", order_dir: "desc", latest_only: true })); + return toFeedPage(papersApi.list({ ...params, order_by: "trending", order_dir: "desc", latest_only: true })); }, { enabled: !following || !!user, initialPage: following ? 0 : 1 }, ); @@ -465,7 +463,7 @@ export const HomeFeed: React.FC<{ mode: FeedMode }> = ({ mode }) => {

Trending Research

-
{mode === "x" ? "Papers people share most on X, picked daily" : "Curated daily from arXiv and Hugging Face"}
+
Curated daily from arXiv and Hugging Face
@@ -578,7 +576,6 @@ export const HomeFeed: React.FC<{ mode: FeedMode }> = ({ mode }) => {
-
@@ -642,7 +639,7 @@ export const HomeFeed: React.FC<{ mode: FeedMode }> = ({ mode }) => { cards={cards} skeleton="card" rootMargin="400px" - empty={error ? null : topicOnly ? <>No papers about your topic match these filters. Widen the date range or . : following ? <>No papers from your topic or follows match these filters. Widen the date range, or follow more from tasks, methods, benchmark, author, or organization pages. : mode === "x" ? "No papers shared on X in this window." : "No papers in this window."} + empty={error ? null : topicOnly ? <>No papers about your topic match these filters. Widen the date range or . : following ? <>No papers from your topic or follows match these filters. Widen the date range, or follow more from tasks, methods, benchmark, author, or organization pages. : "No papers in this window."} /> )} @@ -653,8 +650,8 @@ export const HomeFeed: React.FC<{ mode: FeedMode }> = ({ mode }) => { }; // Routes render the public feeds through this; FollowingView renders the following mode. -const FeedView: React.FC<{ xTrending?: boolean }> = ({ xTrending = false }) => ( - +const FeedView: React.FC = () => ( + ); export default FeedView; diff --git a/frontend/src/views/FollowingView.test.tsx b/frontend/src/views/FollowingView.test.tsx index ca6e1cc2..2edc1e8c 100644 --- a/frontend/src/views/FollowingView.test.tsx +++ b/frontend/src/views/FollowingView.test.tsx @@ -22,7 +22,6 @@ function renderFeed(path = '/following') { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render( } />} /> - } /> ); } beforeEach(() => { @@ -57,7 +56,7 @@ it('renders following in the existing tab row with standard paper cards and thum expect(screen.getByText('Full abstract.')).toBeInTheDocument(); const tab = screen.getByRole('button', { name: 'following' }); expect(tab).toHaveClass('active'); - expect(tab.parentElement?.textContent).toBe('trendingtrending (X)following'); + expect(tab.parentElement?.textContent).toBe('trendingfollowing'); expect(screen.queryByText('Discover')).not.toBeInTheDocument(); expect(papersApi.list).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: 'trending' })); @@ -415,17 +414,8 @@ it('shows the feed for an account that follows only a topic', async () => { expect(await screen.findByText('A paper')).toBeInTheDocument(); expect(screen.queryByRole('heading', { name: 'Build your personal research feed' })).not.toBeInTheDocument(); }); -it('lists papers shared on X under the trending (X) tab, over a month window by default', async () => { - const xTrending = vi.spyOn(papersApi, 'xTrending').mockResolvedValue({ count: 1, results: [paper], next_page: null, previous_page: null } as never); +it('has no separate trending (X) tab: X likes are part of the trending score', async () => { renderFeed('/'); - fireEvent.click(await screen.findByRole('button', { name: 'trending (X)' })); - expect(await screen.findByText('A paper')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'trending (X)' })).toHaveClass('active'); - expect(screen.getByRole('button', { name: 'trending' })).not.toHaveClass('active'); - expect(screen.getByText('Papers people share most on X, picked daily')).toBeInTheDocument(); - expect(xTrending).toHaveBeenCalledWith(expect.objectContaining({ page: 1, page_size: 30, include_resources: true })); - const { start_date, end_date } = xTrending.mock.calls[0][0]!; - expect(start_date && end_date && start_date < end_date).toBeTruthy(); - expect((Date.parse(end_date!) - Date.parse(start_date!)) / 86_400_000).toBeGreaterThan(7); - expect(researchApi.following).not.toHaveBeenCalled(); + expect(await screen.findByRole('button', { name: 'trending' })).toHaveClass('active'); + expect(screen.queryByRole('button', { name: 'trending (X)' })).not.toBeInTheDocument(); });