From e1942770a6dd7feabc6120ba8f7c844e30b670ba Mon Sep 17 00:00:00 2001
From: jerry <1772030600@qq.com>
Date: Tue, 10 Feb 2026 17:43:53 +0800
Subject: [PATCH 1/2] feat(web): add saved papers list with sorting and status
actions
---
web/src/app/papers/page.tsx | 87 +----
.../components/research/SavedPapersList.tsx | 339 ++++++++++++++++++
2 files changed, 351 insertions(+), 75 deletions(-)
create mode 100644 web/src/components/research/SavedPapersList.tsx
diff --git a/web/src/app/papers/page.tsx b/web/src/app/papers/page.tsx
index 91ccc31..0609cee 100644
--- a/web/src/app/papers/page.tsx
+++ b/web/src/app/papers/page.tsx
@@ -1,78 +1,15 @@
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import Link from "next/link"
-import { FileText, Download, GitBranch } from "lucide-react"
-import { fetchPapers } from "@/lib/api"
+import SavedPapersList from "@/components/research/SavedPapersList"
-export default async function PapersPage() {
- const papers = await fetchPapers()
-
- return (
-
-
-
Papers Library
-
-
-
-
-
-
-
- Title
- Venue
- Authors
- Citations
- Status
- Actions
-
-
-
- {papers.map((paper) => (
-
-
-
-
- {paper.title}
-
-
- {paper.tags.map(tag => (
- {tag}
- ))}
-
-
- {paper.venue}
- {paper.authors}
- {paper.citations}
-
-
- {paper.status}
-
-
-
- {paper.status === "Reproduced" && (
-
- )}
-
-
-
- ))}
-
-
-
+export default function PapersPage() {
+ return (
+
+
+
+
Papers Library
+
Saved papers from registry feedback and reading states.
- )
+
+
+
+ )
}
diff --git a/web/src/components/research/SavedPapersList.tsx b/web/src/components/research/SavedPapersList.tsx
new file mode 100644
index 0000000..64038f9
--- /dev/null
+++ b/web/src/components/research/SavedPapersList.tsx
@@ -0,0 +1,339 @@
+"use client"
+
+import { useCallback, useEffect, useMemo, useState } from "react"
+import Link from "next/link"
+import { Loader2, RefreshCw } from "lucide-react"
+
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
+
+
+type SavedPaperSort = "saved_at" | "judge_score" | "published_at"
+
+type ReadingStatus = "unread" | "reading" | "read" | "archived"
+
+type SavedPaperItem = {
+ paper: {
+ id: number
+ title: string
+ authors?: string[]
+ source?: string
+ venue?: string
+ url?: string
+ external_url?: string
+ published_at?: string | null
+ }
+ saved_at?: string | null
+ reading_status?: {
+ status?: string
+ updated_at?: string | null
+ } | null
+ latest_judge?: {
+ overall?: number | null
+ recommendation?: string | null
+ } | null
+}
+
+type SavedPapersResponse = {
+ user_id: string
+ items: SavedPaperItem[]
+}
+
+const PAGE_SIZE_OPTIONS = [10, 20, 50]
+const SORT_OPTIONS: Array<{ value: SavedPaperSort; label: string }> = [
+ { value: "saved_at", label: "Saved Time" },
+ { value: "judge_score", label: "Judge Score" },
+ { value: "published_at", label: "Published Time" },
+]
+
+function formatDate(value?: string | null): string {
+ if (!value) return "-"
+ const dt = new Date(value)
+ if (Number.isNaN(dt.getTime())) return "-"
+ return dt.toLocaleString()
+}
+
+function formatJudge(value?: number | null): string {
+ if (typeof value !== "number") return "-"
+ return `${value.toFixed(2)} / 5.0`
+}
+
+function normalizeStatus(value?: string | null): ReadingStatus {
+ if (value === "reading" || value === "read" || value === "archived") return value
+ return "unread"
+}
+
+export default function SavedPapersList() {
+ const [items, setItems] = useState
([])
+ const [sortBy, setSortBy] = useState("saved_at")
+ const [pageSize, setPageSize] = useState(20)
+ const [page, setPage] = useState(1)
+ const [loading, setLoading] = useState(true)
+ const [refreshTick, setRefreshTick] = useState(0)
+ const [error, setError] = useState(null)
+ const [updatingPaperId, setUpdatingPaperId] = useState(null)
+
+ const loadSavedPapers = useCallback(async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ const qs = new URLSearchParams({
+ sort_by: sortBy,
+ limit: "500",
+ user_id: "default",
+ })
+ const res = await fetch(`/api/research/papers/saved?${qs.toString()}`)
+ if (!res.ok) {
+ throw new Error(await res.text())
+ }
+ const payload = (await res.json()) as SavedPapersResponse
+ setItems(payload.items || [])
+ setPage(1)
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : String(err)
+ setError(detail)
+ setItems([])
+ } finally {
+ setLoading(false)
+ }
+ }, [sortBy])
+
+ useEffect(() => {
+ loadSavedPapers().catch(() => {})
+ }, [loadSavedPapers, refreshTick])
+
+ const totalPages = useMemo(() => {
+ return Math.max(1, Math.ceil(items.length / pageSize))
+ }, [items.length, pageSize])
+
+ const pagedItems = useMemo(() => {
+ const safePage = Math.min(page, totalPages)
+ const start = (safePage - 1) * pageSize
+ return items.slice(start, start + pageSize)
+ }, [items, page, pageSize, totalPages])
+
+ const updateReadingStatus = useCallback(
+ async (paperId: number, status: ReadingStatus, markSaved: boolean | null = null) => {
+ setUpdatingPaperId(paperId)
+ setError(null)
+ try {
+ const body: Record = {
+ user_id: "default",
+ status,
+ metadata: {},
+ }
+ if (markSaved !== null) {
+ body.mark_saved = markSaved
+ }
+
+ const res = await fetch(`/api/research/papers/${paperId}/status`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ })
+
+ if (!res.ok) {
+ throw new Error(await res.text())
+ }
+
+ if (markSaved === false) {
+ setItems((prev) => prev.filter((row) => row.paper.id !== paperId))
+ return
+ }
+
+ setItems((prev) =>
+ prev.map((row) => {
+ if (row.paper.id !== paperId) return row
+ return {
+ ...row,
+ reading_status: {
+ ...row.reading_status,
+ status,
+ updated_at: new Date().toISOString(),
+ },
+ }
+ }),
+ )
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : String(err)
+ setError(detail)
+ } finally {
+ setUpdatingPaperId(null)
+ }
+ },
+ [],
+ )
+
+ return (
+
+
+
+
+ Saved Papers
+
+ View saved items, sort by score/time, and update reading status.
+
+
+
+
+
+
+
+
+
+
+ {error ? {error}
: null}
+
+
+ {loading ? (
+
+ Loading saved papers...
+
+ ) : items.length === 0 ? (
+ No saved papers yet.
+ ) : (
+ <>
+
+
+
+
+ Title
+ Source
+ Saved
+ Judge
+ Status
+ Actions
+
+
+
+ {pagedItems.map((item) => {
+ const paper = item.paper
+ const status = normalizeStatus(item.reading_status?.status)
+ const updating = updatingPaperId === paper.id
+ return (
+
+
+ {paper.title}
+
+ {(paper.authors || []).slice(0, 4).join(", ") || "Unknown authors"}
+
+ {paper.venue ? {paper.venue}
: null}
+
+
+ {paper.source || "unknown"}
+
+
+ {formatDate(item.saved_at)}
+ Published: {formatDate(paper.published_at)}
+
+
+ {formatJudge(item.latest_judge?.overall)}
+ {item.latest_judge?.recommendation ? (
+ {item.latest_judge.recommendation}
+ ) : null}
+
+
+ {status}
+
+
+
+
+
+
+
+ )
+ })}
+
+
+
+
+
+ Showing {(Math.min(page, totalPages) - 1) * pageSize + 1} -{" "}
+ {Math.min(Math.min(page, totalPages) * pageSize, items.length)} of {items.length}
+
+
+
+
+ Page {Math.min(page, totalPages)} / {totalPages}
+
+
+
+
+ >
+ )}
+
+
+ )
+}
From 00355dd359cd3fcf8f7711ac9f1ae6c761b60311 Mon Sep 17 00:00:00 2001
From: jerry <1772030600@qq.com>
Date: Tue, 10 Feb 2026 22:28:47 +0800
Subject: [PATCH 2/2] fix(web): address pagination and action-specific loading
in saved papers
---
.../components/research/SavedPapersList.tsx | 37 +++++++++++++------
1 file changed, 26 insertions(+), 11 deletions(-)
diff --git a/web/src/components/research/SavedPapersList.tsx b/web/src/components/research/SavedPapersList.tsx
index 64038f9..e37ba93 100644
--- a/web/src/components/research/SavedPapersList.tsx
+++ b/web/src/components/research/SavedPapersList.tsx
@@ -41,6 +41,8 @@ type SavedPapersResponse = {
items: SavedPaperItem[]
}
+type UpdatingAction = "toggleRead" | "unsave"
+
const PAGE_SIZE_OPTIONS = [10, 20, 50]
const SORT_OPTIONS: Array<{ value: SavedPaperSort; label: string }> = [
{ value: "saved_at", label: "Saved Time" },
@@ -73,7 +75,7 @@ export default function SavedPapersList() {
const [loading, setLoading] = useState(true)
const [refreshTick, setRefreshTick] = useState(0)
const [error, setError] = useState(null)
- const [updatingPaperId, setUpdatingPaperId] = useState(null)
+ const [updatingAction, setUpdatingAction] = useState<{ paperId: number; action: UpdatingAction } | null>(null)
const loadSavedPapers = useCallback(async () => {
setLoading(true)
@@ -115,8 +117,13 @@ export default function SavedPapersList() {
}, [items, page, pageSize, totalPages])
const updateReadingStatus = useCallback(
- async (paperId: number, status: ReadingStatus, markSaved: boolean | null = null) => {
- setUpdatingPaperId(paperId)
+ async (
+ paperId: number,
+ status: ReadingStatus,
+ markSaved: boolean | null = null,
+ action: UpdatingAction,
+ ) => {
+ setUpdatingAction({ paperId, action })
setError(null)
try {
const body: Record = {
@@ -160,7 +167,7 @@ export default function SavedPapersList() {
const detail = err instanceof Error ? err.message : String(err)
setError(detail)
} finally {
- setUpdatingPaperId(null)
+ setUpdatingAction(null)
}
},
[],
@@ -199,7 +206,10 @@ export default function SavedPapersList() {
id="saved-page-size"
className="h-9 rounded-md border bg-background px-2 text-sm"
value={String(pageSize)}
- onChange={(event) => setPageSize(Number(event.target.value || 20))}
+ onChange={(event) => {
+ setPageSize(Number(event.target.value || 20))
+ setPage(1)
+ }}
>
{PAGE_SIZE_OPTIONS.map((size) => (