From fd22cf9b0780c1a4102d222831244264cceb586c Mon Sep 17 00:00:00 2001 From: Dynamite2003 Date: Thu, 18 Dec 2025 10:53:01 +0800 Subject: [PATCH 1/5] bugfix: fix frontend router bug --- frontend/src/app/page.tsx | 4 +- frontend/src/components/Recommendations.tsx | 156 ++++++++++++++++++-- 2 files changed, 143 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index b4dad71..8efab1c 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -14,6 +14,7 @@ export default function Home() { useEffect(() => { const storedToken = getAccessToken(); if (!storedToken) { + router.replace("/auth"); return; } @@ -23,8 +24,9 @@ export default function Home() { }) .catch(() => { setCurrentUser(null); + router.replace("/auth"); }); - }, []); + }, [router]); const displayName = currentUser?.full_name || "用户"; diff --git a/frontend/src/components/Recommendations.tsx b/frontend/src/components/Recommendations.tsx index a4e5f67..99ec562 100644 --- a/frontend/src/components/Recommendations.tsx +++ b/frontend/src/components/Recommendations.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getAccessToken } from "@/lib/auth"; import { @@ -24,6 +24,7 @@ const SOURCE_OPTIONS = [ { label: "Semantic Scholar", value: "semantic_scholar" }, { label: "OpenAlex", value: "openalex" }, ]; +const SOURCE_OPTION_VALUES = SOURCE_OPTIONS.map((option) => option.value); interface Paper { id?: string; @@ -53,6 +54,64 @@ const TITLE_CHAR_PATTERN = /[\p{L}\p{N}]/gu; type FolderSelection = number | "unfiled"; const FOLLOW_SCHOLARS_KEY = "ir_follow_scholars"; const FOLLOW_ORGS_KEY = "ir_follow_orgs"; +const RECOMMENDATIONS_CACHE_PREFIX = "ir_recommendations_cache"; + +interface RecommendationsCacheEntry { + date: string; + activeTag?: string | null; + papers?: Paper[]; + selectedSources?: string[]; + scholarFilter?: string | null; + orgFilter?: string | null; + sourceErrors?: Record | null; + lastRequestInfo?: string | null; +} + +function buildCacheKey(userId?: number | null) { + return `${RECOMMENDATIONS_CACHE_PREFIX}_${userId ?? "guest"}`; +} + +function getTodayDateKey() { + return new Date().toISOString().slice(0, 10); +} + +function readRecommendationsCache(userId?: number | null): RecommendationsCacheEntry | null { + if (typeof window === "undefined") { + return null; + } + const cacheKey = buildCacheKey(userId); + const raw = window.localStorage.getItem(cacheKey); + if (!raw) { + return null; + } + try { + const parsed = JSON.parse(raw) as RecommendationsCacheEntry; + if (parsed.date !== getTodayDateKey()) { + window.localStorage.removeItem(cacheKey); + return null; + } + return parsed; + } catch (error) { + console.error("Failed to parse recommendation cache", error); + window.localStorage.removeItem(cacheKey); + return null; + } +} + +function writeRecommendationsCache(userId: number | null, entry: Omit) { + if (typeof window === "undefined") { + return; + } + const payload: RecommendationsCacheEntry = { + ...entry, + date: getTodayDateKey(), + }; + try { + window.localStorage.setItem(buildCacheKey(userId), JSON.stringify(payload)); + } catch (error) { + console.error("Failed to write recommendation cache", error); + } +} function computeMatchScore( paper: Paper, @@ -354,6 +413,8 @@ async function fetchRecommendationPapers(query: string, sources: string[]) { export default function Recommendations() { const token = getAccessToken(); + const currentUserRef = useRef(null); + const restoringFromCacheRef = useRef(false); const [tagInput, setTagInput] = useState(""); const [tags, setTags] = useState(DEFAULT_TAGS); const [papers, setPapers] = useState([]); @@ -371,9 +432,7 @@ export default function Recommendations() { const [tagsSyncing, setTagsSyncing] = useState(false); const [tagSyncError, setTagSyncError] = useState(null); const [sourceErrors, setSourceErrors] = useState | null>(null); - const [selectedSources, setSelectedSources] = useState( - SOURCE_OPTIONS.map((option) => option.value) - ); + const [selectedSources, setSelectedSources] = useState(() => [...SOURCE_OPTION_VALUES]); const [hideSavedPapers, setHideSavedPapers] = useState(true); const [savedPaperKeys, setSavedPaperKeys] = useState>(new Set()); const [savedPapersLoading, setSavedPapersLoading] = useState(false); @@ -392,6 +451,40 @@ export default function Recommendations() { const [isCreatingFolderInline, setIsCreatingFolderInline] = useState(false); const [inlineFolderError, setInlineFolderError] = useState(null); + const applyCachedRecommendations = useCallback((entry: RecommendationsCacheEntry) => { + restoringFromCacheRef.current = true; + setPapers(entry.papers ?? []); + setSourceErrors(entry.sourceErrors ?? null); + setActiveTag(entry.activeTag ?? null); + setLastRequestInfo( + entry.lastRequestInfo ? `${entry.lastRequestInfo} · 缓存` : "使用缓存的推荐结果", + ); + setLoading(false); + setError(null); + setSelectedSources( + entry.selectedSources && entry.selectedSources.length + ? [...entry.selectedSources] + : [...SOURCE_OPTION_VALUES], + ); + setActiveScholarFilter(entry.scholarFilter ?? null); + setActiveOrgFilter(entry.orgFilter ?? null); + setTimeout(() => { + restoringFromCacheRef.current = false; + }, 0); + }, []); + + const restoreCachedRecommendations = useCallback( + (userId: number | null) => { + const cached = readRecommendationsCache(userId); + if (!cached) { + return false; + } + applyCachedRecommendations(cached); + return true; + }, + [applyCachedRecommendations], + ); + const visiblePapers = useMemo(() => { if (!hideSavedPapers) { return papers; @@ -419,11 +512,14 @@ export default function Recommendations() { : `${visiblePapers.length} 条候选`; useEffect(() => { + if (restoringFromCacheRef.current) { + return; + } const baseTag = activeTag ?? profileDirections[0] ?? tags[0]; if (!baseTag) { return; } - fetchRecommendations(baseTag); + fetchRecommendations(baseTag, undefined, { bypassCache: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeScholarFilter, activeOrgFilter]); @@ -484,7 +580,21 @@ export default function Recommendations() { } }, []); - async function fetchRecommendations(query: string, customSources?: string[]) { + async function fetchRecommendations( + query: string, + customSources?: string[], + options?: { bypassCache?: boolean }, + ) { + const shouldBypassCache = options?.bypassCache ?? false; + const userId = currentUserRef.current?.id ?? null; + if (!shouldBypassCache) { + const cached = readRecommendationsCache(userId); + if (cached) { + applyCachedRecommendations(cached); + return; + } + } + setLoading(true); setError(null); setPapers([]); @@ -501,9 +611,8 @@ export default function Recommendations() { const tagQuery = combinedTags.join(" "); const filters = [activeScholarFilter, activeOrgFilter].filter(Boolean); const contextQuery = [tagQuery || query, ...filters].filter(Boolean).join(" "); - setLastRequestInfo( - `POST /api/v1/academic/search · sources: ${sources.join(", ")} · query: ${contextQuery}`, - ); + const requestLabel = `POST /api/v1/academic/search · sources: ${sources.join(", ")} · query: ${contextQuery}`; + setLastRequestInfo(requestLabel); const result = await fetchRecommendationPapers(contextQuery, sources); const enriched = result.papers.map((paper, idx) => { const profileScore = computeMatchScore( @@ -523,6 +632,15 @@ export default function Recommendations() { enriched.sort((a, b) => b.score - a.score); setPapers(enriched.map((item) => item.paper)); setSourceErrors(result.sourceErrors); + writeRecommendationsCache(userId, { + activeTag: query, + papers: enriched.map((item) => item.paper), + selectedSources: [...sources], + scholarFilter: activeScholarFilter, + orgFilter: activeOrgFilter, + sourceErrors: result.sourceErrors ?? null, + lastRequestInfo: requestLabel, + }); } catch (e) { console.error("fetchArxivPapers error:", e); const message = e instanceof Error ? e.message : "请求失败"; @@ -546,6 +664,7 @@ export default function Recommendations() { research_interests: nextTags.length ? nextTags.join(", ") : null, }; const updated = await updateCurrentUser(token, payload); + currentUserRef.current = updated; setCurrentUser(updated); } catch (err) { console.error("Failed to persist tags", err); @@ -673,7 +792,7 @@ export default function Recommendations() { if (newTag) { const nextTags = [newTag, ...tags.filter((tag) => tag !== newTag)]; setTags(nextTags); - fetchRecommendations(newTag); + fetchRecommendations(newTag, undefined, { bypassCache: true }); setTagInput(""); void persistTags(nextTags); return; @@ -681,7 +800,7 @@ export default function Recommendations() { const tagToRefresh = activeTag ?? tags[0]; if (tagToRefresh) { - fetchRecommendations(tagToRefresh); + fetchRecommendations(tagToRefresh, undefined, { bypassCache: true }); } } @@ -698,7 +817,7 @@ export default function Recommendations() { const nextTag = activeTag && activeTag !== tagToRemove ? activeTag : updatedTags[0]; - fetchRecommendations(nextTag); + fetchRecommendations(nextTag, undefined, { bypassCache: true }); } const handleToggleSource = (sourceValue: string) => { @@ -712,7 +831,7 @@ export default function Recommendations() { const nextTag = activeTag ?? tags[0]; if (nextTag) { - fetchRecommendations(nextTag, nextSources); + fetchRecommendations(nextTag, nextSources, { bypassCache: true }); } return nextSources; @@ -739,6 +858,7 @@ export default function Recommendations() { if (!isMounted) { return; } + currentUserRef.current = profile; setCurrentUser(profile); const storedTags = parseResearchInterests(profile.research_interests); const mergedTags = [...storedTags]; @@ -787,6 +907,10 @@ export default function Recommendations() { setProfileSignals(initialSignals); setProfileScholars(savedScholars); setProfileOrgs(savedOrgs); + const restored = restoreCachedRecommendations(currentUserRef.current?.id ?? null); + if (restored) { + return; + } const initialTag = initialTags[0]; if (initialTag) { fetchRecommendations(initialTag); @@ -799,7 +923,7 @@ export default function Recommendations() { isMounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadSavedPaperKeys]); + }, [loadSavedPaperKeys, restoreCachedRecommendations]); return (
@@ -823,11 +947,11 @@ export default function Recommendations() { className="text-xs text-slate-500 underline-offset-2 hover:underline" onClick={() => { if (activeTag) { - fetchRecommendations(activeTag); + fetchRecommendations(activeTag, undefined, { bypassCache: true }); return; } if (tags[0]) { - fetchRecommendations(tags[0]); + fetchRecommendations(tags[0], undefined, { bypassCache: true }); } }} > From 36e522f151e32775839e586d9ef97456c2b1704d Mon Sep 17 00:00:00 2001 From: Dynamite2003 Date: Thu, 18 Dec 2025 11:05:14 +0800 Subject: [PATCH 2/5] bugfix: enlarge pdf upload display effect --- frontend/src/app/upload/page.tsx | 95 +++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/upload/page.tsx b/frontend/src/app/upload/page.tsx index a8c7dec..45e99fc 100644 --- a/frontend/src/app/upload/page.tsx +++ b/frontend/src/app/upload/page.tsx @@ -8,6 +8,20 @@ import { getAccessToken } from "@/lib/auth"; import { useLibraryFolders } from "@/hooks/use-library-folders"; const ACCEPTED_MIME_TYPES = ["application/pdf", "application/x-pdf"]; +const MAX_FILE_SIZE = 25 * 1024 * 1024; + +function formatFileSize(bytes?: number | null) { + if (!bytes || bytes <= 0) { + return "0 MB"; + } + if (bytes < 1024) { + return `${bytes} B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} export default function UploadPaperPage() { const token = getAccessToken(); @@ -33,6 +47,8 @@ export default function UploadPaperPage() { })), [folders], ); + const selectedFileSizeLabel = selectedFile ? formatFileSize(selectedFile.size) : null; + const hasSelectedFile = Boolean(selectedFile); if (!token) { return ( @@ -100,7 +116,7 @@ export default function UploadPaperPage() { return; } - if (file.size > 25 * 1024 * 1024) { + if (file.size > MAX_FILE_SIZE) { setError("单个文件大小不能超过 25MB"); setSelectedFile(null); return; @@ -129,30 +145,59 @@ export default function UploadPaperPage() {