Skip to content

feat(Research page): Rebuild the Research page and set it as the homepage#49

Merged
CJBshuosi merged 4 commits into
jerry609:masterfrom
CJBshuosi:master
Feb 11, 2026
Merged

feat(Research page): Rebuild the Research page and set it as the homepage#49
CJBshuosi merged 4 commits into
jerry609:masterfrom
CJBshuosi:master

Conversation

@CJBshuosi

@CJBshuosi CJBshuosi commented Feb 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #48

Signed-off-by: LIU BOYU oor2020@163.com

Summary by CodeRabbit

  • New Features

    • Redesigned Research page with a Claude-inspired, single-column, search-centric interface
    • Full track management: create, edit, manage tracks (modals), activate tracks, and clear track memory
    • Improved search UX: search box, track selector/pills, paper cards, and detailed search results with loading states
    • Paper interactions: save, like, and mark as not relevant
    • Navigation update: Research becomes the home route; Dashboard moved to its own route with stats and activity
  • Documentation

    • Added comprehensive design specification for the Research page redesign
  • Tests

    • Added unit and integration tests covering track update scenarios

…page

Closes jerry609#48

Signed-off-by: LIU BOYU <oor2020@163.com>
feat(Research page): Rebuild the Research page and set it as the homepage
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @CJBshuosi, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant user experience improvement by redesigning the Research page to be more intuitive and efficient. The new layout, inspired by modern AI interfaces, simplifies the process of finding and managing research papers. By making this redesigned page the new homepage and relocating the existing dashboard, the update enhances the application's primary user flow, focusing on immediate research tasks while maintaining access to broader system overviews.

Highlights

  • Research Page Redesign: The Research page has been completely rebuilt with a minimal, search-centric UI inspired by Claude.ai, moving from a complex two-column layout to a streamlined single-column design. This includes new components for search input, track selection, and paper result display.
  • Homepage Update: The redesigned Research page is now set as the default homepage for the application, providing a focused entry point for users.
  • Dedicated Dashboard Page: The previous homepage content, which served as a dashboard, has been moved to a new dedicated '/dashboard' route, ensuring its functionality remains accessible.
  • Enhanced Track Management: New backend API endpoints and frontend UI components have been introduced to allow users to create, edit, activate, and clear the memory of their research tracks more effectively. This includes modals for creation, editing, and a comprehensive management view.
  • Interactive Paper Recommendations: The search results now display paper recommendations in a card-based layout, featuring interactive feedback actions (save, like, dislike) and visual loading states, improving user engagement and relevance feedback.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • docs/research_page_redesign.md
    • Added a comprehensive design document detailing the objectives, target state, specifications, user flows, visual design, and technical implementation of the new Research page.
  • src/paperbot/api/routes/research.py
    • Introduced TrackUpdateRequest model for partial updates to research tracks.
    • Added a new PATCH /research/tracks/{track_id} endpoint to allow updating track details.
  • src/paperbot/infrastructure/stores/research_store.py
    • Implemented the update_track method to handle persistence of changes to research track attributes like name, description, keywords, venues, and methods.
  • web/src/app/api/research/tracks/[trackId]/route.ts
    • Added a new API route to proxy PATCH and GET requests for individual research tracks to the backend.
  • web/src/app/dashboard/page.tsx
    • Created a new page component to house the previous dashboard content, establishing a dedicated route for it.
  • web/src/app/page.tsx
    • Modified the root page to render the new ResearchPageNew component, making the redesigned research page the application's homepage.
  • web/src/app/research/page.tsx
    • Updated the existing research page to also render the new ResearchPageNew component, ensuring consistency.
  • web/src/components/layout/Sidebar.tsx
    • Adjusted the sidebar navigation links to reflect the new homepage (Research) and the new location of the Dashboard.
  • web/src/components/research/CreateTrackModal.tsx
    • Added a new modal component for creating new research tracks with name, description, and keywords.
  • web/src/components/research/EditTrackModal.tsx
    • Added a new modal component for editing the details of an existing research track.
  • web/src/components/research/ManageTracksModal.tsx
    • Added a new modal component that provides a centralized interface for viewing, editing, and clearing the memory of all research tracks.
  • web/src/components/research/PaperCard.tsx
    • Added a new component to display individual paper recommendations, including metadata, abstract preview, and interactive feedback buttons (save, like, dislike).
  • web/src/components/research/ResearchPageNew.tsx
    • Added the primary component for the redesigned research page, orchestrating state management, API interactions, and UI rendering for search and track management.
  • web/src/components/research/SearchBox.tsx
    • Added a new component for the main search input, featuring integrated track selection and submission logic.
  • web/src/components/research/SearchResults.tsx
    • Added a new component responsible for displaying paper search results, including loading skeletons and handling no-results scenarios.
  • web/src/components/research/TrackPills.tsx
    • Added a new component to show quick-access clickable pills for frequently used or active research tracks.
  • web/src/components/research/TrackSelector.tsx
    • Added a new dropdown component for selecting and managing active research tracks.
  • web/src/components/research/index.ts
    • Updated the barrel file to export all newly created research-related components.
  • web/src/components/ui/skeleton.tsx
    • Added a generic Skeleton UI component for displaying loading placeholders.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Redesigns the Research page into a Claude-style, single-column, search-first interface; adds backend support for PATCH /research/tracks/{id}; introduces multiple new React components for search, results, track management and modals; moves original Dashboard to /dashboard and makes Research the new homepage; adds tests and utility helpers.

Changes

Cohort / File(s) Summary
Design Document
docs/research_page_redesign.md
New comprehensive design spec (layout, components, flows, API, migration plan, visuals).
Backend: Track Update
src/paperbot/api/routes/research.py, src/paperbot/infrastructure/stores/research_store.py
New TrackUpdateRequest, PATCH /research/tracks/{track_id} endpoint, and SqlAlchemyResearchStore.update_track(...) with IntegrityError handling and embedding precompute scheduling.
API Proxy Route
web/src/app/api/research/tracks/[trackId]/route.ts
Node runtime route forwarding GET and PATCH for individual tracks to backend.
Routing / Navigation
web/src/app/page.tsx, web/src/app/research/page.tsx, web/src/app/dashboard/page.tsx, web/src/components/layout/Sidebar.tsx
Homepage now renders ResearchPageNew, original dashboard moved to /dashboard, sidebar reordered (Research first, Dashboard second).
Research UI — Page & Index
web/src/components/research/ResearchPageNew.tsx, web/src/components/research/index.ts
New central page component orchestrating search, tracks, modals, API calls, and feedback; barrel export for research components.
Research UI — Core Widgets
web/src/components/research/SearchBox.tsx, web/src/components/research/SearchResults.tsx, web/src/components/research/PaperCard.tsx, web/src/components/ui/skeleton.tsx
Search input, results container with skeletons/animations, paper card with feedback actions, and Skeleton UI component.
Research UI — Track Management
web/src/components/research/TrackSelector.tsx, web/src/components/research/TrackPills.tsx, web/src/components/research/CreateTrackModal.tsx, web/src/components/research/EditTrackModal.tsx, web/src/components/research/ManageTracksModal.tsx
Dropdown, quick-access pills, and three modals for creating, editing, and managing tracks (including clear-memory actions).
Client Utilities
web/src/lib/utils.ts
Added `safeHref(url?: string
Tests
tests/unit/test_research_store_track_update.py, tests/integration/test_research_track_routes.py
Unit and integration tests covering track update logic, error cases (404/409/400), ownership checks, and partial updates.
Minor UI Logic
web/src/components/research/ResearchDashboard.tsx
Added client-side duplicate-name guard in createTrack to preempt API error.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SearchBox as SearchBox (Client)
    participant Page as ResearchPageNew (Client)
    participant API as Backend API
    participant Store as Research Store / DB

    User->>SearchBox: enter query & submit
    SearchBox->>Page: onSearch(query, activeTrack)
    Page->>API: POST /research/context (query, track_id)
    API->>Store: query papers, build context
    Store-->>API: papers + reasoning
    API-->>Page: ContextPack (papers, reasons)
    Page->>Page: set results, isSearching=false
    Page->>SearchBox: update UI (results visible)

    User->>Page: open ManageTracks / CreateTrack
    Page->>API: PATCH /research/tracks/{id}  OR POST /research/tracks
    API->>Store: update/create track, handle IntegrityError
    Store-->>API: updated track
    API-->>Page: TrackResponse
    Page->>Page: refresh tracks, schedule embeddings (background)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code with nimble paws,
Made pills and boxes, modals, claws,
Tracks now hum and papers sing,
Search found light — the homepage spring,
A rabbit cheers for this new thing!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: rebuilding the Research page and setting it as the homepage, with specific focus on the research redesign.
Linked Issues check ✅ Passed All coding objectives from issue #48 are met: new components (SearchBox, TrackSelector, TrackPills, PaperCard, SearchResults, CreateTrackModal, EditTrackModal, ManageTracksModal, ResearchPageNew, skeleton), backend additions (PATCH endpoint and update_track method), and navigation changes.
Out of Scope Changes check ✅ Passed All changes are directly related to the Research page redesign. The DashboardPage component in web/src/app/dashboard/page.tsx is a supporting change to accommodate moving the original dashboard to /dashboard.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully rebuilds the Research page with a modern, search-centric UI, setting it as the new homepage. However, a security audit revealed a high-severity Insecure Direct Object Reference (IDOR) vulnerability in the backend API, stemming from user identification relying on a request parameter rather than a secure session. Furthermore, a medium-severity Cross-Site Scripting (XSS) vulnerability was identified in the frontend where external URLs are rendered without proper sanitization. Beyond these security concerns, there are also minor suggestions for improving maintainability and adhering to React best practices. Addressing these security issues is critical, and incorporating the suggested code improvements will further enhance this otherwise well-executed and extensive update.

track_id: int,
req: TrackUpdateRequest,
background_tasks: BackgroundTasks,
user_id: str = "default",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The API endpoint accepts a user_id parameter (defaulting to "default") which is used to identify the user for data access and modification. Since this parameter is taken from the request (query string) and not from a secure session or token, any user can impersonate another user by providing their user_id in the request. This allows unauthorized access to and modification of research tracks belonging to other users.

To remediate this, remove user_id as a request parameter. Instead, implement a secure authentication mechanism and derive the authenticated user's ID from the secure session or token on the backend.

</h3>
{paper.url && (
<a
href={paper.url}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The paper.url property is rendered directly into the href attribute of an <a> tag without sanitization. If an attacker can control the URL (e.g., via a malicious entry in the external paper database), they can inject a javascript: URL to execute arbitrary code in the context of the user's session when the link is clicked.

To remediate this, sanitize the URL before rendering it. Ensure it uses an allowed protocol (e.g., http:, https:) and does not contain malicious scripts.

Comment thread src/paperbot/api/routes/research.py Outdated
Comment on lines +142 to +152
update_data: Dict[str, Any] = {}
if req.name is not None:
update_data["name"] = req.name
if req.description is not None:
update_data["description"] = req.description
if req.keywords is not None:
update_data["keywords"] = req.keywords
if req.venues is not None:
update_data["venues"] = req.venues
if req.methods is not None:
update_data["methods"] = req.methods

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual construction of update_data can be simplified and made more maintainable by using Pydantic's built-in methods. This will automatically include any new fields added to TrackUpdateRequest in the future.

Using model_dump(exclude_unset=True) (for Pydantic v2) or dict(exclude_unset=True) (for Pydantic v1) is a more robust approach.

Suggested change
update_data: Dict[str, Any] = {}
if req.name is not None:
update_data["name"] = req.name
if req.description is not None:
update_data["description"] = req.description
if req.keywords is not None:
update_data["keywords"] = req.keywords
if req.venues is not None:
update_data["venues"] = req.venues
if req.methods is not None:
update_data["methods"] = req.methods
update_data = req.model_dump(exclude_unset=True)

Comment thread web/src/app/dashboard/page.tsx Outdated
Comment on lines +77 to +85
{trends.map((topic, i) => (
<Badge
key={i}
variant="secondary"
className="text-xs cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors"
>
{topic.text}
</Badge>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the array index as a key is an anti-pattern in React, especially for dynamic lists that can be reordered or filtered. This can lead to unpredictable UI behavior and performance issues. It's recommended to use a unique and stable identifier from the data itself, such as a unique ID or string from the topic object.

Suggested change
{trends.map((topic, i) => (
<Badge
key={i}
variant="secondary"
className="text-xs cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors"
>
{topic.text}
</Badge>
))}
{trends.map((topic) => (
<Badge
key={topic.text} // Assuming topic.text is unique. If not, a unique ID from the topic object is preferred.
variant="secondary"
className="text-xs cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors"
>
{topic.text}
</Badge>
))}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/paperbot/infrastructure/stores/research_store.py (1)

11-13: ⚠️ Potential issue | 🟡 Minor

Duplicate import.

normalize_arxiv_id and normalize_doi are imported twice (lines 11 and 13).

Fix
 from paperbot.domain.paper_identity import normalize_arxiv_id, normalize_doi
 
-from paperbot.domain.paper_identity import normalize_arxiv_id, normalize_doi
-
🤖 Fix all issues with AI agents
In `@src/paperbot/api/routes/research.py`:
- Around line 135-161: Add tests covering the new PATCH endpoint function
update_track and the store method SqlAlchemyResearchStore.update_track: write
unit tests for (1) successful partial update (verify returned TrackResponse
contains updated fields and that _schedule_embedding_precompute/background task
is triggered with the correct track_id), (2) empty update payload returns HTTP
400, and (3) non-existent track returns HTTP 404; for the store method add tests
for updating individual fields and for returning None when track_id not found.
In tests, call the router path that hits update_track and mock/spy the
_research_store.update_track and _schedule_embedding_precompute (or inject a
test BackgroundTasks) to assert they are invoked with expected arguments; also
include input fixtures for TrackUpdateRequest variations to cover name,
description, keywords, venues, and methods updates.

In `@src/paperbot/infrastructure/stores/research_store.py`:
- Around line 182-218: The update_track method can raise an unhandled
sqlalchemy.exc.IntegrityError when changing a track name to one that already
exists; modify update_track to catch IntegrityError around session.commit(),
call session.rollback() on error, and then raise the same kind of domain-level
exception used in create_track (or a new DuplicateTrackNameError) so the API
layer can map it to a 409 Conflict instead of returning a 500; reference the
update_track function, the session.commit() call, and the create_track handling
as the model to follow.

In `@web/src/app/dashboard/page.tsx`:
- Around line 14-21: DashboardPage uses Promise.all to call fetchStats,
fetchTrendingTopics, fetchPipelineTasks, fetchReadingQueue, and fetchLLMUsage so
a single network error will reject the whole page; change to Promise.allSettled
(or individually try/catch) and map the settled results to either the .value or
a safe fallback for each corresponding variable (stats, trends, tasks,
readingQueue, llmUsage), ensuring the component renders with defaults instead of
throwing; update any downstream code that assumes non-null responses
accordingly.

In `@web/src/components/research/CreateTrackModal.tsx`:
- Around line 39-58: The form is being cleared unconditionally in handleCreate,
causing user input loss when onCreateTrack (parent
ResearchPageNew.handleCreateTrack) fails or swallows errors; change handleCreate
to only reset via setName/setDescription/setKeywords after a confirmed success —
either by letting onCreateTrack re-throw errors from
ResearchPageNew.handleCreateTrack and wrapping the await onCreateTrack(...) in
try/catch (reset in the try after await), or by validating a returned success
flag from onCreateTrack before clearing the fields.

In `@web/src/components/research/EditTrackModal.tsx`:
- Around line 55-72: handleUpdate currently closes the modal unconditionally
after calling onUpdateTrack so failures are hidden; change it to await
onUpdateTrack inside a try/catch and only call onOpenChange(false) on success
(or re-throw the caught error) so the modal remains open on failure, and/or
update ResearchPageNew.handleUpdateTrack to re-throw after setting its error
state so handleUpdate can detect failures; reference handleUpdate,
onUpdateTrack, ResearchPageNew.handleUpdateTrack, and onOpenChange when applying
this fix.

In `@web/src/components/research/ResearchPageNew.tsx`:
- Around line 38-45: The current fetchJson function attaches the full response
body to the thrown Error (via res.text()), which can leak server internals into
the UI; change fetchJson to throw a safe, generic message like `${res.status}
${res.statusText}` and avoid embedding the full response body in the Error that
flows to ResearchPageNew; if you still need the body for debugging, log it to
console or include it conditionally only in development (e.g., use
process.env.NODE_ENV === 'development') so that the UI-rendered error (the <pre>
in ResearchPageNew) never displays raw server stack traces or internal details.
- Around line 245-280: The handleFeedback function currently calls fetchJson
without error handling which can cause unhandled promise rejections; wrap the
await fetchJson(...) call in a try/catch inside handleFeedback, catch any error,
setError(...) with a user-friendly message (and optionally console.error the
error), and ensure you do not modify global loading state (PaperCard handles
loading) so that failures surface to the UI without crashing the parent click
handler.

In `@web/src/components/research/SearchBox.tsx`:
- Around line 103-114: The icon-only search Button in SearchBox.tsx lacks an
accessible label; update the Button (the element using onClick={handleSubmit}
and props disabled={disabled || isSearching || !query.trim()}) to include an
aria-label that reflects state (e.g., aria-label={isSearching ? "Searching…" :
"Search"}), so screen readers get a meaningful label for both the idle and
loading states; ensure the label is updated dynamically alongside isSearching
and do not remove the visual icon.
🧹 Nitpick comments (10)
web/src/app/dashboard/page.tsx (1)

77-85: Prefer a stable key over array index.

topic.text is unique and more semantically meaningful than the array index.

Suggested fix
-              {trends.map((topic, i) => (
-                <Badge
-                  key={i}
+              {trends.map((topic) => (
+                <Badge
+                  key={topic.text}
web/src/components/research/PaperCard.tsx (1)

50-83: Action errors are silently swallowed — user gets no feedback on failure.

All three handlers (handleSave, handleLike, handleDislike) use try/finally without a catch. If the callback throws, the loading state resets but the user sees no indication that the action failed. Consider adding a catch block to surface a toast or temporary error state.

web/src/components/research/SearchResults.tsx (1)

110-119: Consider computing stagger delay instead of enumerating indices.

The explicit idx === 0 && ... through idx >= 5 is functional but won't scale well if the number of visible cards changes. A computed approach is more maintainable:

Suggested refactor
               className={cn(
                 "animate-in fade-in slide-in-from-bottom-2",
-                // Staggered animation delay
-                idx === 0 && "duration-300",
-                idx === 1 && "duration-300 delay-[50ms]",
-                idx === 2 && "duration-300 delay-[100ms]",
-                idx === 3 && "duration-300 delay-[150ms]",
-                idx === 4 && "duration-300 delay-[200ms]",
-                idx >= 5 && "duration-300 delay-[250ms]"
+                "duration-300",
+                `delay-[${Math.min(idx * 50, 250)}ms]`
               )}
docs/research_page_redesign.md (1)

16-58: Add language identifiers to fenced code blocks.

Multiple fenced code blocks throughout the document lack language specifiers, which triggers markdownlint MD040 warnings. For ASCII art diagrams, use ```text to satisfy the linter and improve rendering.

web/src/app/research/page.tsx (1)

1-9: Duplicate page: identical to web/src/app/page.tsx.

Both / (page.tsx) and /research (research/page.tsx) render the exact same markup — a <div> wrapper around <ResearchPageNew />. Consider having one redirect to the other (e.g., redirect("/research") from the root page, or vice versa) to avoid maintaining two identical files.

Option: redirect from root to /research

In web/src/app/page.tsx:

-import ResearchPageNew from "@/components/research/ResearchPageNew"
+import { redirect } from "next/navigation"

-export default function HomePage() {
-  return (
-    <div className="flex-1 bg-stone-50/50 dark:bg-background">
-      <ResearchPageNew />
-    </div>
-  )
+export default function HomePage() {
+  redirect("/research")
 }
web/src/components/research/TrackPills.tsx (1)

65-69: The "+N more" indicator is not actionable.

Users can see that additional tracks exist but have no way to access them from this component. Consider making the indicator clickable — e.g., opening the Manage Tracks modal — to avoid a dead-end UX.

Make "+N more" open the manage modal

Add an onManageTracks prop and wire it:

 interface TrackPillsProps {
   tracks: Track[]
   activeTrackId: number | null
   onSelectTrack: (trackId: number) => void
   onNewTrack: () => void
+  onManageTracks?: () => void
   disabled?: boolean
   maxVisible?: number
 }
       {hasMore && (
-        <span className="text-sm sm:text-base text-muted-foreground">
+        <button
+          className="text-sm sm:text-base text-muted-foreground hover:underline"
+          onClick={onManageTracks}
+          disabled={disabled}
+        >
           +{tracks.length - maxVisible} more
-        </span>
+        </button>
       )}
web/src/components/research/ResearchPageNew.tsx (2)

54-76: Shared loading flag guards unrelated operations.

A single loading boolean is used by activateTrack, handleCreateTrack, handleUpdateTrack, and confirmClearMemory. If any two overlap (e.g., user activates a track while an update is in flight), the first finally block clears loading prematurely. Consider per-action loading states or a counter.


56-56: Hardcoded userId — worth a TODO.

userId is always "default". This is presumably intentional for the current phase, but a comment or TODO would make it clear this needs auth integration.

Add a TODO
-  const [userId] = useState("default")
+  // TODO: Replace with authenticated user ID once auth is integrated
+  const [userId] = useState("default")
web/src/components/research/ManageTracksModal.tsx (1)

103-112: Consider adding a confirmation step before clearing track memory.

This is a destructive, irreversible action that resets a track's context. A single click on the Trash2 icon button (which has no label text) could easily be triggered accidentally. Consider either an AlertDialog confirmation or an optimistic undo pattern.

web/src/components/research/index.ts (1)

18-20: Legacy exports may discourage cleanup.

Exporting ResearchDashboard and SavedPapersList from the barrel keeps them discoverable and easy to import. If they're truly superseded by the new components, consider removing these re-exports (or adding a @deprecated JSDoc) so consumers naturally migrate away.

Comment on lines +135 to +161
@router.patch("/research/tracks/{track_id}", response_model=TrackResponse)
def update_track(
track_id: int,
req: TrackUpdateRequest,
background_tasks: BackgroundTasks,
user_id: str = "default",
):
update_data: Dict[str, Any] = {}
if req.name is not None:
update_data["name"] = req.name
if req.description is not None:
update_data["description"] = req.description
if req.keywords is not None:
update_data["keywords"] = req.keywords
if req.venues is not None:
update_data["venues"] = req.venues
if req.methods is not None:
update_data["methods"] = req.methods

if not update_data:
raise HTTPException(status_code=400, detail="No fields to update")

track = _research_store.update_track(user_id=user_id, track_id=track_id, **update_data)
if not track:
raise HTTPException(status_code=404, detail="Track not found")
_schedule_embedding_precompute(background_tasks, user_id=user_id, track_ids=[track_id])
return TrackResponse(track=track)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing tests for new PATCH endpoint and store method.

The new update_track endpoint and SqlAlchemyResearchStore.update_track introduce behavior changes but no corresponding tests are included. As per coding guidelines, "If behavior changes, add or update tests".

🤖 Prompt for AI Agents
In `@src/paperbot/api/routes/research.py` around lines 135 - 161, Add tests
covering the new PATCH endpoint function update_track and the store method
SqlAlchemyResearchStore.update_track: write unit tests for (1) successful
partial update (verify returned TrackResponse contains updated fields and that
_schedule_embedding_precompute/background task is triggered with the correct
track_id), (2) empty update payload returns HTTP 400, and (3) non-existent track
returns HTTP 404; for the store method add tests for updating individual fields
and for returning None when track_id not found. In tests, call the router path
that hits update_track and mock/spy the _research_store.update_track and
_schedule_embedding_precompute (or inject a test BackgroundTasks) to assert they
are invoked with expected arguments; also include input fixtures for
TrackUpdateRequest variations to cover name, description, keywords, venues, and
methods updates.

Comment on lines +182 to +218
def update_track(
self,
*,
user_id: str,
track_id: int,
name: Optional[str] = None,
description: Optional[str] = None,
keywords: Optional[List[str]] = None,
venues: Optional[List[str]] = None,
methods: Optional[List[str]] = None,
) -> Optional[Dict[str, Any]]:
now = _utcnow()
with self._provider.session() as session:
row = session.execute(
select(ResearchTrackModel).where(
ResearchTrackModel.user_id == user_id, ResearchTrackModel.id == track_id
)
).scalar_one_or_none()
if row is None:
return None

if name is not None:
row.name = name.strip()
if description is not None:
row.description = description.strip()
if keywords is not None:
row.keywords_json = _dump_list(keywords)
if venues is not None:
row.venues_json = _dump_list(venues)
if methods is not None:
row.methods_json = _dump_list(methods)

row.updated_at = now
session.add(row)
session.commit()
session.refresh(row)
return self._track_to_dict(row)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

IntegrityError not handled when renaming to a duplicate name.

The research_tracks table has a UniqueConstraint("user_id", "name"). If a user updates a track's name to one that already exists, the session.commit() on line 216 will raise an unhandled IntegrityError, resulting in a 500 response. The existing create_track method handles this case explicitly.

Suggested fix
             row.updated_at = now
             session.add(row)
-            session.commit()
-            session.refresh(row)
-            return self._track_to_dict(row)
+            try:
+                session.commit()
+            except IntegrityError:
+                session.rollback()
+                return None  # or raise a more specific error
+            session.refresh(row)
+            return self._track_to_dict(row)

Note: Returning None would map to a 404 at the API layer, which isn't ideal for a uniqueness conflict. You may want to raise a distinct exception that the route can catch and return as a 409 Conflict instead.

🤖 Prompt for AI Agents
In `@src/paperbot/infrastructure/stores/research_store.py` around lines 182 - 218,
The update_track method can raise an unhandled sqlalchemy.exc.IntegrityError
when changing a track name to one that already exists; modify update_track to
catch IntegrityError around session.commit(), call session.rollback() on error,
and then raise the same kind of domain-level exception used in create_track (or
a new DuplicateTrackNameError) so the API layer can map it to a 409 Conflict
instead of returning a 500; reference the update_track function, the
session.commit() call, and the create_track handling as the model to follow.

Comment thread web/src/app/dashboard/page.tsx
Comment thread web/src/components/research/CreateTrackModal.tsx
Comment thread web/src/components/research/EditTrackModal.tsx
Comment on lines +38 to +45
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, init)
if (!res.ok) {
const text = await res.text().catch(() => "")
throw new Error(`${res.status} ${res.statusText} ${text}`.trim())
}
return res.json() as Promise<T>
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

fetchJson error message may leak server internals to the UI.

The error includes the full response body (res.text()), which is rendered verbatim in a <pre> tag on Line 413. If the server returns stack traces or internal details in error responses, they'll be shown to the user.

🤖 Prompt for AI Agents
In `@web/src/components/research/ResearchPageNew.tsx` around lines 38 - 45, The
current fetchJson function attaches the full response body to the thrown Error
(via res.text()), which can leak server internals into the UI; change fetchJson
to throw a safe, generic message like `${res.status} ${res.statusText}` and
avoid embedding the full response body in the Error that flows to
ResearchPageNew; if you still need the body for debugging, log it to console or
include it conditionally only in development (e.g., use process.env.NODE_ENV ===
'development') so that the UI-rendered error (the <pre> in ResearchPageNew)
never displays raw server stack traces or internal details.

Comment on lines +245 to +280
async function handleFeedback(
paperId: string,
action: string,
rank?: number,
paper?: Paper
): Promise<void> {
// Don't set global loading - PaperCard handles its own loading state
setError(null)
const body: Record<string, unknown> = {
user_id: userId,
track_id: activeTrackId,
paper_id: paperId,
action,
weight: 0.0,
context_run_id: contextPack?.context_run_id ?? null,
context_rank: typeof rank === "number" ? rank : undefined,
metadata: {},
}

// Include paper metadata for save action
if (action === "save" && paper) {
body.paper_title = paper.title
body.paper_abstract = paper.abstract || ""
body.paper_authors = paper.authors || []
body.paper_year = paper.year
body.paper_venue = paper.venue
body.paper_citation_count = paper.citation_count
body.paper_url = paper.url
}

await fetchJson(`/api/research/papers/feedback`, {
method: "POST",
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

handleFeedback has no error handling — unhandled promise rejection.

Every other API call in this component wraps fetchJson in try/catch, but handleFeedback does not. If the feedback request fails, the rejected promise will propagate to the child component's click handler and surface as an unhandled rejection, with no user-visible feedback.

Add try/catch
   async function handleFeedback(
     paperId: string,
     action: string,
     rank?: number,
     paper?: Paper
   ): Promise<void> {
-    // Don't set global loading - PaperCard handles its own loading state
-    setError(null)
+    try {
+      setError(null)
       const body: Record<string, unknown> = {
         // ... existing body construction
       }

       await fetchJson(`/api/research/papers/feedback`, {
         method: "POST",
         body: JSON.stringify(body),
         headers: { "Content-Type": "application/json" },
       })
+    } catch (e) {
+      setError(String(e))
+    }
   }
🤖 Prompt for AI Agents
In `@web/src/components/research/ResearchPageNew.tsx` around lines 245 - 280, The
handleFeedback function currently calls fetchJson without error handling which
can cause unhandled promise rejections; wrap the await fetchJson(...) call in a
try/catch inside handleFeedback, catch any error, setError(...) with a
user-friendly message (and optionally console.error the error), and ensure you
do not modify global loading state (PaperCard handles loading) so that failures
surface to the UI without crashing the parent click handler.

Comment thread web/src/components/research/SearchBox.tsx
Closes jerry609#48

Signed-off-by: LIU BOYU <oor2020@163.com>
Fix(Research page): add -- unit test and fix security issues

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/src/app/dashboard/page.tsx`:
- Around line 41-49: The Search input (Input component) and Import Button render
without handlers; add an onSubmit/onKeyDown handler for the Input (e.g.,
implement onSearchSubmit or handleSearchChange + onEnter to trigger search) and
wire the Button to an onClick handler (e.g., handleImportClick) that calls the
existing import/search logic or dispatches the relevant action; if they are
intentionally inactive, mark them disabled (disabled prop) and add a clear TODO
comment explaining future behavior so intent is explicit.
🧹 Nitpick comments (4)
web/src/components/research/ResearchPageNew.tsx (1)

38-45: fetchJson is duplicated across ResearchDashboard.tsx and ResearchPageNew.tsx.

Both files define identical fetchJson implementations. Consider extracting it to a shared utility (e.g., in @/lib/utils.ts or a dedicated @/lib/api.ts).

src/paperbot/infrastructure/stores/research_store.py (1)

11-13: Duplicate import on lines 11 and 13.

normalize_arxiv_id and normalize_doi are imported twice from the same module.

Remove duplicate
 from paperbot.domain.paper_identity import normalize_arxiv_id, normalize_doi
 
-from paperbot.domain.paper_identity import normalize_arxiv_id, normalize_doi
-
tests/unit/test_research_store_track_update.py (1)

9-12: Consider a shared pytest fixture to reduce boilerplate.

Every test repeats the same 2-line store setup. A @pytest.fixture would reduce duplication.

Example fixture
+@pytest.fixture
+def store(tmp_path):
+    db_url = f"sqlite:///{tmp_path / 'test.db'}"
+    return SqlAlchemyResearchStore(db_url=db_url, auto_create_schema=True)
+
+
-def test_update_track_name_success(tmp_path):
+def test_update_track_name_success(store):
     """Test updating track name successfully."""
-    db_url = f"sqlite:///{tmp_path / 'test.db'}"
-    store = SqlAlchemyResearchStore(db_url=db_url, auto_create_schema=True)
-
     # Create a track
     track = store.create_track(user_id="test", name="Original Name", activate=False)
tests/integration/test_research_track_routes.py (1)

79-154: Consider adding tests for venues and methods fields.

The TrackUpdateRequest model also supports venues and methods (per the route definition), but there are no tests exercising those fields. Adding at least one test for each would improve confidence that serialization (_dump_list/_load_list) works correctly for all list fields.

Comment on lines +41 to +49
<div className="flex w-full sm:max-w-sm items-center gap-1.5 bg-muted/50 p-1 rounded-md border">
<Search className="ml-2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="arXiv ID, URL or search..."
className="h-8 border-none bg-transparent shadow-none focus-visible:ring-0 text-sm"
/>
<Button size="sm" className="h-7 text-xs">Import</Button>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Search input and Import button are non-functional.

The search bar and Import button lack event handlers — they render as inert UI elements. If these are intentional placeholders for future work, consider adding a disabled attribute or a TODO comment to make that explicit.

🤖 Prompt for AI Agents
In `@web/src/app/dashboard/page.tsx` around lines 41 - 49, The Search input (Input
component) and Import Button render without handlers; add an onSubmit/onKeyDown
handler for the Input (e.g., implement onSearchSubmit or handleSearchChange +
onEnter to trigger search) and wire the Button to an onClick handler (e.g.,
handleImportClick) that calls the existing import/search logic or dispatches the
relevant action; if they are intentionally inactive, mark them disabled
(disabled prop) and add a clear TODO comment explaining future behavior so
intent is explicit.

@CJBshuosi
CJBshuosi merged commit 29b5c92 into jerry609:master Feb 11, 2026
4 checks passed
This was referenced Mar 12, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Rebuild the Research page and set it as the homepage

1 participant