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) => (