diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5a2253e..16bf0c4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/typography": "^0.5.19", "@uiw/react-md-editor": "^4.0.4", + "framer-motion": "^12.23.26", "lucide-react": "^0.552.0", "next": "15.5.4", "pdfjs-dist": "^3.11.174", @@ -1484,6 +1485,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1550,6 +1552,7 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -2176,6 +2179,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3305,6 +3309,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3479,6 +3484,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3882,6 +3888,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -6655,6 +6688,21 @@ "node": ">=10" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7308,6 +7356,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7317,6 +7366,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -8496,7 +8546,8 @@ "version": "4.1.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -8570,6 +8621,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8746,6 +8798,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/frontend/package.json b/frontend/package.json index 249f3f5..6d0d5ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@tailwindcss/typography": "^0.5.19", "@uiw/react-md-editor": "^4.0.4", + "framer-motion": "^12.23.26", "lucide-react": "^0.552.0", "next": "15.5.4", "pdfjs-dist": "^3.11.174", diff --git a/frontend/src/app/academic/page.tsx b/frontend/src/app/academic/page.tsx index f4240b7..20113ad 100644 --- a/frontend/src/app/academic/page.tsx +++ b/frontend/src/app/academic/page.tsx @@ -529,7 +529,7 @@ export default function AcademicSearchPage() { onChange={(event) => setQuestion(event.target.value)} placeholder="与 AI 助手对话,提出你的研究问题…" rows={3} - className="w-full resize-none rounded-3xl border border-slate-200 bg-transparent px-5 pb-12 pr-16 pt-3 text-sm leading-relaxed text-slate-900 placeholder:text-slate-500 transition focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:opacity-60 dark:border-slate-600 dark:bg-transparent dark:text-slate-100 dark:placeholder:text-slate-400" + className="w-full resize-none rounded-3xl border border-slate-200 bg-white/95 px-5 pb-12 pr-16 pt-3 text-sm leading-relaxed text-slate-900 placeholder:text-slate-500 shadow-[0_10px_40px_-25px_rgba(15,23,42,0.5)] transition focus:border-blue-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:opacity-60 dark:border-slate-600 dark:bg-slate-900/85 dark:text-slate-100 dark:placeholder:text-slate-400" disabled={loading} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -681,7 +681,7 @@ export default function AcademicSearchPage() { onChange={(event) => setQuestion(event.target.value)} placeholder="描述你的研究问题、关键词或阅读意图…" rows={4} - className="w-full resize-none rounded-3xl border border-slate-200 bg-transparent px-5 pb-16 pr-16 pt-4 text-sm leading-relaxed text-slate-900 placeholder:text-slate-500 transition focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:opacity-60 dark:border-slate-600 dark:bg-transparent dark:text-slate-100 dark:placeholder:text-slate-400" + className="w-full resize-none rounded-3xl border border-slate-200 bg-white/95 px-5 pb-16 pr-16 pt-4 text-sm leading-relaxed text-slate-900 placeholder:text-slate-500 shadow-[0_10px_50px_-25px_rgba(15,23,42,0.4)] transition focus:border-blue-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:opacity-60 dark:border-slate-600 dark:bg-slate-900/80 dark:text-slate-100 dark:placeholder:text-slate-400" disabled={loading} /> diff --git a/frontend/src/app/auth/page.tsx b/frontend/src/app/auth/page.tsx index 50e9140..f3c85aa 100644 --- a/frontend/src/app/auth/page.tsx +++ b/frontend/src/app/auth/page.tsx @@ -1,6 +1,7 @@ "use client"; import { AuthForm } from "@/components/auth-form"; +import InsightWorkflow from "@/components/marketing/insight-workflow"; export default function AuthPage() { return ( @@ -10,20 +11,8 @@ export default function AuthPage() { >
-
- +
+
diff --git a/frontend/src/app/notes/page.tsx b/frontend/src/app/notes/page.tsx index ad110a5..cf7236d 100644 --- a/frontend/src/app/notes/page.tsx +++ b/frontend/src/app/notes/page.tsx @@ -192,46 +192,64 @@ export default function NotesPage() {

笔记分类

文件夹

- - 共 {folderGroups.length} - + + + 新建 + +

+ 共 {folderGroups.length} 个分类,支持展开查看论文及其关联笔记 +

{loading ? (
加载中...
) : (!folderGroups.some((group) => group.papers.length > 0) && unlinkedNotes.length === 0) ? (
还没有笔记,先创建一条吧。
) : ( -
+
{folderGroups.map(({ folder, papers: folderPapers, noteCount }) => { const folderKey = `folder-${folder.id}`; const isFolderOpen = expandedGroups[folderKey] ?? true; + const folderSummary = `${folderPapers.length} 论文 / ${noteCount} 笔记`; return ( -
+
{isFolderOpen && ( -
+
{folderPapers.length === 0 && (
该文件夹暂无论文
)} @@ -239,14 +257,17 @@ export default function NotesPage() { const paperKey = `paper-${paper.id}`; const isPaperOpen = expandedGroups[paperKey] ?? false; return ( -
+
))}
@@ -279,20 +302,36 @@ export default function NotesPage() { })} {unlinkedNotes.length > 0 && ( -
+
{expandedGroups["unlinked"] && ( -
+
{unlinkedNotes.map((note) => ( - @@ -342,30 +370,16 @@ export default function SmartReadingPage() { onMouseUp={() => { if (isDragging && draftRatios) { setSplitRatios(draftRatios as [number, number, number]); - setDraftRatios(null); } - setIsDragging(false); - setActiveDivider(null); - if (renderResumeTimerRef.current) { - clearTimeout(renderResumeTimerRef.current); - } - renderResumeTimerRef.current = setTimeout(() => { - setRenderPaused(false); - }, 100); + resetLayoutInteraction(); + scheduleRenderResume(100); }} onMouseLeave={() => { if (isDragging && draftRatios) { setSplitRatios(draftRatios as [number, number, number]); - setDraftRatios(null); - } - setIsDragging(false); - setActiveDivider(null); - if (renderResumeTimerRef.current) { - clearTimeout(renderResumeTimerRef.current); } - renderResumeTimerRef.current = setTimeout(() => { - setRenderPaused(false); - }, 100); + resetLayoutInteraction(); + scheduleRenderResume(100); }} > {(() => { @@ -410,6 +424,7 @@ export default function SmartReadingPage() { onMouseDown={() => { if (renderResumeTimerRef.current) { clearTimeout(renderResumeTimerRef.current); + renderResumeTimerRef.current = null; } setRenderPaused(true); setIsDragging(true); @@ -445,6 +460,7 @@ export default function SmartReadingPage() { onMouseDown={() => { if (renderResumeTimerRef.current) { clearTimeout(renderResumeTimerRef.current); + renderResumeTimerRef.current = null; } setRenderPaused(true); setIsDragging(true); 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() {