├── .npmrc ├── .prettierignore ├── src ├── lib │ ├── components │ │ └── ui │ │ │ ├── sonner │ │ │ ├── index.ts │ │ │ └── sonner.svelte │ │ │ ├── input │ │ │ ├── index.ts │ │ │ └── input.svelte │ │ │ ├── label │ │ │ ├── index.ts │ │ │ └── label.svelte │ │ │ ├── skeleton │ │ │ ├── index.ts │ │ │ └── skeleton.svelte │ │ │ ├── separator │ │ │ ├── index.ts │ │ │ └── separator.svelte │ │ │ ├── avatar │ │ │ ├── index.ts │ │ │ ├── avatar-fallback.svelte │ │ │ ├── avatar-image.svelte │ │ │ └── avatar.svelte │ │ │ ├── button │ │ │ ├── index.ts │ │ │ └── button.svelte │ │ │ ├── dialog │ │ │ ├── dialog-description.svelte │ │ │ ├── dialog-title.svelte │ │ │ ├── dialog-header.svelte │ │ │ ├── dialog-footer.svelte │ │ │ ├── dialog-overlay.svelte │ │ │ ├── index.ts │ │ │ └── dialog-content.svelte │ │ │ ├── popover │ │ │ ├── index.ts │ │ │ └── popover-content.svelte │ │ │ ├── select │ │ │ ├── select-group-heading.svelte │ │ │ ├── select-separator.svelte │ │ │ ├── select-scroll-up-button.svelte │ │ │ ├── select-scroll-down-button.svelte │ │ │ ├── select-trigger.svelte │ │ │ ├── index.ts │ │ │ ├── select-item.svelte │ │ │ └── select-content.svelte │ │ │ ├── dropdown-menu │ │ │ ├── dropdown-menu-separator.svelte │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ ├── dropdown-menu-label.svelte │ │ │ ├── dropdown-menu-item.svelte │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ ├── dropdown-menu-content.svelte │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ ├── dropdown-menu-checkbox-item.svelte │ │ │ └── index.ts │ │ │ └── card │ │ │ ├── card-content.svelte │ │ │ ├── card-footer.svelte │ │ │ ├── card-header.svelte │ │ │ ├── card-description.svelte │ │ │ ├── card.svelte │ │ │ ├── index.ts │ │ │ └── card-title.svelte │ ├── index.ts │ ├── assets │ │ ├── logo │ │ │ └── logo.png │ │ └── icons │ │ │ ├── google.png │ │ │ ├── twitter.png │ │ │ ├── youtube.png │ │ │ └── huggingface.png │ ├── auth_client.ts │ ├── constants.ts │ ├── auth.ts │ ├── utils.ts │ └── auth_functions.ts ├── state │ ├── guest_mode_state.svelte.ts │ ├── papers_list.svelte.ts │ ├── comment_state.svelte.ts │ ├── input_state.svelte.ts │ ├── ai_conversation_state.svelte.ts │ └── each_paper_state.svelte.ts ├── routes │ ├── api │ │ ├── types │ │ │ └── types.ts │ │ ├── utils │ │ │ ├── session_manager.ts │ │ │ ├── save_papers_to_db.ts │ │ │ ├── add_values_to_papers.ts │ │ │ └── search_and_clean_papers.ts │ │ ├── constants.ts │ │ ├── get_ai_api_key │ │ │ └── +server.ts │ │ ├── like_papers │ │ │ └── +server.ts │ │ ├── get_liked_papers │ │ │ └── +server.ts │ │ ├── get_bookmarked_papers │ │ │ └── +server.ts │ │ ├── search_papers │ │ │ └── +server.ts │ │ ├── save_ai_api_key │ │ │ └── +server.ts │ │ ├── comment_on_paper │ │ │ └── +server.ts │ │ ├── get_paper_comments │ │ │ └── +server.ts │ │ ├── bookmark_papers │ │ │ └── +server.ts │ │ ├── delete_comment │ │ │ └── +server.ts │ │ └── ai_chat │ │ │ └── +server.ts │ ├── comments │ │ └── papers │ │ │ └── [paperid] │ │ │ ├── +layout.svelte │ │ │ └── +page.svelte │ ├── +layout.svelte │ ├── +page.svelte │ ├── liked_papers_page │ │ └── +page.svelte │ ├── bookmarks_page │ │ └── +page.svelte │ ├── auth │ │ └── sign_in │ │ │ └── +page.svelte │ └── homepage │ │ └── +page.svelte ├── components │ ├── skeleton │ │ ├── comments_skeleton.svelte │ │ ├── feed_skeletons.svelte │ │ ├── comment_skeleton.svelte │ │ └── skeleton_paper.svelte │ ├── remarks │ │ ├── better_auth_remark.svelte │ │ └── arxiv_remark.svelte │ ├── navigation_buttons.svelte │ ├── each_paper │ │ ├── authors.svelte │ │ ├── published_date_and_id.svelte │ │ ├── paper_title.svelte │ │ ├── summary.svelte │ │ ├── each_paper.svelte │ │ └── interactions.svelte │ ├── main_input │ │ ├── label_and_input_box.svelte │ │ ├── input_settings.svelte │ │ ├── ai_settings.svelte │ │ ├── advanced_search.svelte │ │ └── main_input.svelte │ ├── footer.svelte │ ├── profile_avatar.svelte │ ├── ai_chat │ │ ├── choose_model.svelte │ │ ├── selected_papers.svelte │ │ └── ai_chat.svelte │ ├── select_all.svelte │ ├── title.svelte │ ├── navigation.svelte │ ├── each_comment │ │ └── each_comment.svelte │ └── each_paper_old.svelte ├── app.d.ts ├── app.html ├── hooks.server.ts ├── db │ └── db.ts └── app.css ├── static ├── favicon.png └── screenshots │ ├── screenshot.jpg │ ├── screenshot1.jpg │ ├── screenshot10.jpg │ ├── screenshot11.jpg │ ├── screenshot12.jpg │ ├── screenshot2.jpg │ ├── screenshot3.jpg │ ├── screenshot4.jpg │ ├── screenshot5.jpg │ ├── screenshot6.jpg │ ├── screenshot7.jpg │ ├── screenshot8.jpg │ └── screenshot9.jpg ├── postcss.config.js ├── vite.config.ts ├── .gitignore ├── .prettierrc ├── .env.example ├── components.json ├── tsconfig.json ├── svelte.config.js ├── eslint.config.js ├── package.json ├── tailwind.config.ts ├── CODE_OF_CONDUCT.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from "./sonner.svelte"; 2 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/state/guest_mode_state.svelte.ts: -------------------------------------------------------------------------------- 1 | export const guestModeState = $state({ 2 | isGuestMode: true 3 | }); 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/assets/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/src/lib/assets/logo/logo.png -------------------------------------------------------------------------------- /src/lib/assets/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/src/lib/assets/icons/google.png -------------------------------------------------------------------------------- /src/lib/assets/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/src/lib/assets/icons/twitter.png -------------------------------------------------------------------------------- /src/lib/assets/icons/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/src/lib/assets/icons/youtube.png -------------------------------------------------------------------------------- /static/screenshots/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot.jpg -------------------------------------------------------------------------------- /src/lib/assets/icons/huggingface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/src/lib/assets/icons/huggingface.png -------------------------------------------------------------------------------- /static/screenshots/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot1.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot10.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot11.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot12.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot2.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot3.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot4.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot5.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot6.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot7.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot8.jpg -------------------------------------------------------------------------------- /static/screenshots/screenshot9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dagmawibabi/ScholarXIVWeb/HEAD/static/screenshots/screenshot9.jpg -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Input, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./label.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./skeleton.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Skeleton, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./separator.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator, 7 | }; 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /src/state/papers_list.svelte.ts: -------------------------------------------------------------------------------- 1 | export const paperListState = $state({ 2 | paperList: [], 3 | bookmarkList: [], 4 | likedPapersList: [], 5 | isGettingBookmarkedPapers: true, 6 | isGettingLikedPapers: true 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes/api/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface searchStringOBJI { 2 | ti?: string; 3 | au?: string; 4 | abs?: string; 5 | co?: string; 6 | jr?: string; 7 | cat?: string; 8 | rn?: string; 9 | id?: string; 10 | all?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/skeleton/comments_skeleton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/remarks/better_auth_remark.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | Powered by Better-Auth 8 |
9 | -------------------------------------------------------------------------------- /src/components/skeleton/feed_skeletons.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/remarks/arxiv_remark.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | Thank you to arXiv for use of its open access interoperability. 8 | 9 |
10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./avatar.svelte"; 2 | import Image from "./avatar-image.svelte"; 3 | import Fallback from "./avatar-fallback.svelte"; 4 | 5 | export { 6 | Root, 7 | Image, 8 | Fallback, 9 | // 10 | Root as Avatar, 11 | Image as AvatarImage, 12 | Fallback as AvatarFallback, 13 | }; 14 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BETTER_AUTH_SECRET='' 2 | BETTER_AUTH_URL=http://localhost:5173 3 | MONGO_URI='' 4 | GEMINIKEY='' 5 | 6 | GOOGLE_CLIENT_ID='' 7 | GOOGLE_CLIENT_SECRET='' 8 | 9 | GITHUB_CLIENT_ID='' 10 | GITHUB_CLIENT_SECRET='' 11 | 12 | HUGGINGFACE_CLIENT_ID='' 13 | HUGGINGFACE_CLIENT_SECRET='' 14 | 15 | TWITTER_CLIENT_ID='' 16 | TWITTER_CLIENT_SECRET='' -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { 2 | type ButtonProps, 3 | type ButtonSize, 4 | type ButtonVariant, 5 | buttonVariants, 6 | } from "./button.svelte"; 7 | 8 | export { 9 | Root, 10 | type ButtonProps as Props, 11 | // 12 | Root as Button, 13 | buttonVariants, 14 | type ButtonProps, 15 | type ButtonSize, 16 | type ButtonVariant, 17 | }; 18 | -------------------------------------------------------------------------------- /src/state/comment_state.svelte.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export const commentState: { 3 | isGettingComments: boolean; 4 | comments: any[]; 5 | paper: any; 6 | isCommenting: boolean; 7 | comment: ''; 8 | } = $state({ 9 | isGettingComments: true, 10 | comments: [], 11 | paper: {}, 12 | isCommenting: false, 13 | comment: '' 14 | }); 15 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src\\app.css", 7 | "baseColor": "zinc" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils", 12 | "ui": "$lib/components/ui", 13 | "hooks": "$lib/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/state/input_state.svelte.ts: -------------------------------------------------------------------------------- 1 | export const inputState = $state({ 2 | searchContent: '', 3 | isSearching: true, 4 | hasSearched: false, 5 | lastSearch: '', 6 | aiInput: '', 7 | advancedSearch: false, 8 | id: '', 9 | ti: '', 10 | au: '', 11 | abs: '', 12 | co: '', 13 | jr: '', 14 | cat: '', 15 | rn: '', 16 | sortBy: 'Sort By', 17 | sortOrder: 'Sort Order', 18 | startIndex: 0, 19 | maxResults: 10, 20 | statusText: '', 21 | statusKeyword: '' 22 | }); 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | import { Popover as PopoverPrimitive } from "bits-ui"; 2 | import Content from "./popover-content.svelte"; 3 | const Root = PopoverPrimitive.Root; 4 | const Trigger = PopoverPrimitive.Trigger; 5 | const Close = PopoverPrimitive.Close; 6 | 7 | export { 8 | Root, 9 | Content, 10 | Trigger, 11 | Close, 12 | // 13 | Root as Popover, 14 | Content as PopoverContent, 15 | Trigger as PopoverTrigger, 16 | Close as PopoverClose, 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/components/navigation_buttons.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 |
8 | 11 |
12 | 13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/lib/auth_client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from 'better-auth/svelte'; 2 | import { anonymousClient } from 'better-auth/client/plugins'; 3 | 4 | export const authClient = createAuthClient({ 5 | baseURL: import.meta.env.BETTER_AUTH_URL!, 6 | plugins: [anonymousClient()] 7 | }); 8 | 9 | export const { 10 | signIn, 11 | signUp, 12 | signOut, 13 | forgetPassword, 14 | changePassword, 15 | changeEmail, 16 | updateUser, 17 | resetPassword, 18 | useSession 19 | } = authClient; 20 | -------------------------------------------------------------------------------- /src/components/each_paper/authors.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |
10 | 11 | {#each paper['authors'] as eachAuthor} 12 |
13 | 14 | {eachAuthor}, 15 | 16 |
17 | {/each} 18 |
19 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar-fallback.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |

15 | {@render children?.()} 16 |

17 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar-image.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/avatar/avatar.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./card.svelte"; 2 | import Content from "./card-content.svelte"; 3 | import Description from "./card-description.svelte"; 4 | import Footer from "./card-footer.svelte"; 5 | import Header from "./card-header.svelte"; 6 | import Title from "./card-title.svelte"; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /src/routes/api/utils/session_manager.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '$lib/auth'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export async function getSession(request: any) { 5 | try { 6 | const authInstance = await auth; 7 | if (!authInstance?.api?.getSession) { 8 | console.error('Auth not properly initialized'); 9 | return null; 10 | } 11 | const session = await authInstance.api.getSession({ 12 | headers: request.headers 13 | }); 14 | return session; 15 | } catch (error) { 16 | console.error('Error getting session:', error); 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/api/constants.ts: -------------------------------------------------------------------------------- 1 | export const baseURL = 'https://export.arxiv.org/api/query?search_query='; 2 | export const pdfBaseURL = 'https://arxiv.org/pdf'; 3 | export const defaultStartIndex = '0'; 4 | export const defaultMaxResults = '20'; 5 | export const defaultSearchFilter = { 6 | ti: '', 7 | au: '', 8 | abs: '', 9 | co: '', 10 | jr: '', 11 | cat: '', 12 | rn: '', 13 | id: '', 14 | all: '' 15 | }; 16 | export const defaultSortBy = 'relevance'; // ["relevance", "lastUpdatedDate", "submittedDate"] 17 | export const defaultSortOrder = 'ascending'; // ["ascending", "descending"] 18 | 19 | // On the Topological Complexity of Maps 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
22 | {@render children?.()} 23 |
24 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-scroll-up-button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
24 | {@render children?.()} 25 |
26 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-scroll-down-button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /src/components/main_input/label_and_input_box.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
17 | 23 |
24 | 25 | {#if label} 26 |
{label}
27 | {/if} 28 |
29 | -------------------------------------------------------------------------------- /src/routes/comments/papers/[paperid]/+layout.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {@render children()} 13 | 14 | 15 | 16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /src/state/ai_conversation_state.svelte.ts: -------------------------------------------------------------------------------- 1 | interface message { 2 | from: string; 3 | content: string; 4 | } 5 | 6 | interface IAIConversation { 7 | conversation: message[]; 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | selectedPapersList: any[]; 10 | selectedPapersIDList: string[]; 11 | models: { name: string; model: string }[]; 12 | currentModel: { name: string; model: string }; 13 | } 14 | 15 | export const aiConversationState = $state({ 16 | conversation: [], 17 | selectedPapersList: [], 18 | selectedPapersIDList: [], 19 | models: [ 20 | { name: 'Gemini', model: 'gemini-1.5-flash' }, 21 | { name: 'Grok', model: 'grok-beta' } 22 | ], 23 | currentModel: { name: 'Gemini', model: 'gemini-1.5-flash' } 24 | }); 25 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | {@render children()} 12 | 13 | 14 | 15 | 16 | {#if page.url.pathname == '/homepage' || page.url.pathname == '/bookmarks_page' || page.url.pathname == '/liked_papers_page'} 17 | 18 | {/if} 19 |
20 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter(), 15 | alias: { 16 | $db: './src/db' 17 | } 18 | } 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /src/components/footer.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 9 | Checkout the 10 | Demo Video 15 | and 16 | Star the Project 21 | ✨ 22 | 23 | 24 | 25 | 26 | Made with 🖤 by Dream Intelligence 27 |
28 | -------------------------------------------------------------------------------- /src/routes/api/utils/save_papers_to_db.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export async function saveToDB(cleanedPapers: any) { 5 | const db = await getDb(); 6 | const papers = db.collection('papers'); 7 | 8 | // Retrieve all existing papers from the database once 9 | const existingPapers = await papers.find({}).toArray(); 10 | const existingIDs = new Set(existingPapers.map((paper) => paper.extractedID)); 11 | 12 | // Collect new papers to insert 13 | const papersToInsert = []; 14 | for (const paper of cleanedPapers) { 15 | if (!existingIDs.has(paper.extractedID)) { 16 | papersToInsert.push(paper); 17 | } 18 | } 19 | 20 | // Insert all new papers at once 21 | if (papersToInsert.length > 0) { 22 | await papers.insertMany(papersToInsert); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | // import { svelteKitHandler } from 'better-auth/svelte-kit'; 2 | // import { mongoClient } from '$db/db'; 3 | // import { auth } from '$lib/auth'; 4 | 5 | // mongoClient 6 | // .connect() 7 | // .then(() => { 8 | // console.log('Connected to MongoDB'); 9 | // }) 10 | // .catch((e) => { 11 | // console.error('MongoDB connection error:', e); 12 | // }); 13 | 14 | // export async function handle({ event, resolve }) { 15 | // return svelteKitHandler({ event, resolve, auth }); 16 | // } 17 | 18 | import { svelteKitHandler } from 'better-auth/svelte-kit'; 19 | import { auth } from '$lib/auth'; 20 | 21 | // Initialize auth when the server starts 22 | const authInstance = await auth; 23 | 24 | export async function handle({ event, resolve }) { 25 | return svelteKitHandler({ event, resolve, auth: authInstance }); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /src/components/profile_avatar.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | {session.data?.user.name[0].toString().toUpperCase()} 18 | 19 | 20 | {#if fullInfo == true} 21 |
22 | 23 | {session.data?.user.name} 24 | 25 | 26 | {session.data?.user.email} 27 | 28 |
29 | {/if} 30 |
31 |
32 | -------------------------------------------------------------------------------- /src/components/each_paper/published_date_and_id.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 | 20 | {readableTime} 21 | 22 |
23 | 24 | 25 |
26 | 27 | 28 | {paper['extractedID']} 29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 9 | 10 | export default ts.config( 11 | includeIgnoreFile(gitignorePath), 12 | js.configs.recommended, 13 | ...ts.configs.recommended, 14 | ...svelte.configs['flat/recommended'], 15 | prettier, 16 | ...svelte.configs['flat/prettier'], 17 | { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.node 22 | } 23 | } 24 | }, 25 | { 26 | files: ['**/*.svelte'], 27 | 28 | languageOptions: { 29 | parserOptions: { 30 | parser: ts.parser 31 | } 32 | } 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | {@render children?.()} 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/skeleton/comment_skeleton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | span]:line-clamp-1", 18 | className 19 | )} 20 | {...restProps} 21 | > 22 | {@render children?.()} 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from "bits-ui"; 2 | 3 | import Title from "./dialog-title.svelte"; 4 | import Footer from "./dialog-footer.svelte"; 5 | import Header from "./dialog-header.svelte"; 6 | import Overlay from "./dialog-overlay.svelte"; 7 | import Content from "./dialog-content.svelte"; 8 | import Description from "./dialog-description.svelte"; 9 | 10 | const Root = DialogPrimitive.Root; 11 | const Trigger = DialogPrimitive.Trigger; 12 | const Close = DialogPrimitive.Close; 13 | const Portal = DialogPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose, 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from "bits-ui"; 2 | 3 | import GroupHeading from "./select-group-heading.svelte"; 4 | import Item from "./select-item.svelte"; 5 | import Content from "./select-content.svelte"; 6 | import Trigger from "./select-trigger.svelte"; 7 | import Separator from "./select-separator.svelte"; 8 | import ScrollDownButton from "./select-scroll-down-button.svelte"; 9 | import ScrollUpButton from "./select-scroll-up-button.svelte"; 10 | 11 | const Root = SelectPrimitive.Root; 12 | const Group = SelectPrimitive.Group; 13 | 14 | export { 15 | Root, 16 | Group, 17 | GroupHeading, 18 | Item, 19 | Content, 20 | Trigger, 21 | Separator, 22 | ScrollDownButton, 23 | ScrollUpButton, 24 | // 25 | Root as Select, 26 | Group as SelectGroup, 27 | GroupHeading as SelectGroupHeading, 28 | Item as SelectItem, 29 | Content as SelectContent, 30 | Trigger as SelectTrigger, 31 | Separator as SelectSeparator, 32 | ScrollDownButton as SelectScrollDownButton, 33 | ScrollUpButton as SelectScrollUpButton, 34 | }; 35 | -------------------------------------------------------------------------------- /src/routes/api/get_ai_api_key/+server.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | import { json } from '@sveltejs/kit'; 3 | import { getSession } from '../utils/session_manager'; 4 | import { ObjectId } from 'mongodb'; 5 | 6 | export async function GET({ request }) { 7 | try { 8 | const db = await getDb(); 9 | const user = db.collection('user'); 10 | 11 | // Get user session 12 | const session = await getSession(request); 13 | const userID = session?.user.id; 14 | 15 | if (!userID) { 16 | return json({ error: 'User not authenticated' }, { status: 401 }); 17 | } 18 | 19 | // Get user's API key from database 20 | const userDoc = await user.findOne({ _id: new ObjectId(userID) }); 21 | 22 | if (!userDoc) { 23 | return json({ error: 'User not found' }, { status: 404 }); 24 | } 25 | 26 | // Return the API key (or null if not set) 27 | return json({ 28 | apiKey: userDoc?.apiKey ?? null 29 | }); 30 | } catch (error) { 31 | console.error('Error fetching API key:', error); 32 | return json({ error: 'Failed to fetch API key' }, { status: 500 }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/popover/popover-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | {#snippet children({ checked })} 23 | 24 | {#if checked} 25 | 26 | {/if} 27 | 28 | {@render childrenProp?.({ checked })} 29 | {/snippet} 30 | 31 | -------------------------------------------------------------------------------- /src/routes/api/like_papers/+server.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | import { json } from '@sveltejs/kit'; 3 | import { getSession } from '../utils/session_manager'; 4 | 5 | const db = await getDb(); 6 | const likedPapers = db.collection('likedpapers'); 7 | 8 | export async function POST({ request }) { 9 | // Get User ID 10 | const session = await getSession(request); 11 | const userID = session?.user.id; 12 | 13 | const { paperID } = await request.json(); 14 | 15 | const newLike = { 16 | userID: userID, 17 | paperID: paperID, 18 | createdAt: new Date().toISOString() 19 | }; 20 | 21 | // Check if it's been liked before 22 | const existingLike = await likedPapers.findOne({ userID: userID, paperID: paperID }); 23 | 24 | // Add or remove like 25 | if (existingLike) { 26 | // Delete the existing like 27 | await likedPapers.deleteOne({ userID: userID, paperID: paperID }); 28 | } else { 29 | await likedPapers.insertOne(newLike); 30 | } 31 | 32 | // Send back updated paper 33 | // const likedPaper = [await papers.findOne({ id: paperID })]; 34 | // const updatedPaper = await addLikeValueToPapers(c, likedPaper); 35 | 36 | // Response 37 | return json(newLike); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/each_paper/paper_title.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 24 | 25 | {paperState.paper['title']} 26 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | {#snippet children({ selected, highlighted })} 26 | 27 | {#if selected} 28 | 29 | {/if} 30 | 31 | {#if childrenProp} 32 | {@render childrenProp({ selected, highlighted })} 33 | {:else} 34 | {label || value} 35 | {/if} 36 | {/snippet} 37 | 38 | -------------------------------------------------------------------------------- /src/routes/api/get_liked_papers/+server.ts: -------------------------------------------------------------------------------- 1 | // import axios from 'axios'; 2 | import { getDb } from '$db/db'; 3 | import { json } from '@sveltejs/kit'; 4 | 5 | import { addValuesToPapers } from '../utils/add_values_to_papers'; 6 | import { getSession } from '../utils/session_manager'; 7 | 8 | const db = await getDb(); 9 | const papers = db.collection('papers'); 10 | const likedPapers = db.collection('likedpapers'); 11 | 12 | export async function GET({ request }) { 13 | // Get User ID 14 | const session = await getSession(request); 15 | const userID = session?.user.id; 16 | 17 | // Get User Liked Papers 18 | const result = await likedPapers.find({ userID: userID }).toArray(); 19 | 20 | // Extract paperIDs from the likes 21 | const paperIDs = result.map((like) => like.paperID); 22 | 23 | // Fetch papers using the extracted paperIDs 24 | const rawLikedPapers = await papers.find({ extractedID: { $in: paperIDs } }).toArray(); 25 | 26 | for (const eachLikedPaper of rawLikedPapers) { 27 | eachLikedPaper.isBookmarked = true; 28 | } 29 | 30 | // Add dynamic values 31 | const likedPapersWithValues = await addValuesToPapers(rawLikedPapers, userID); 32 | 33 | // Response 34 | return json(likedPapersWithValues); 35 | } 36 | -------------------------------------------------------------------------------- /src/routes/api/get_bookmarked_papers/+server.ts: -------------------------------------------------------------------------------- 1 | // import axios from 'axios'; 2 | import { getDb } from '$db/db'; 3 | import { json } from '@sveltejs/kit'; 4 | 5 | const db = await getDb(); 6 | const papers = db.collection('papers'); 7 | const bookmarks = db.collection('bookmarks'); 8 | 9 | import { addValuesToPapers } from '../utils/add_values_to_papers'; 10 | import { getSession } from '../utils/session_manager'; 11 | 12 | export async function GET({ request }) { 13 | // Get User ID 14 | const session = await getSession(request); 15 | const userID = session?.user.id; 16 | 17 | // Get User Bookmarks 18 | const result = await bookmarks.find({ userID: userID }).toArray(); 19 | 20 | // Extract paperIDs from the bookmarks 21 | const paperIDs = result.map((bookmark) => bookmark.paperID); 22 | 23 | // Fetch papers using the extracted paperIDs 24 | const rawBookmarks = await papers.find({ extractedID: { $in: paperIDs } }).toArray(); 25 | 26 | for (const eachBookmarks of rawBookmarks) { 27 | eachBookmarks.isBookmarked = true; 28 | } 29 | 30 | // Add dynamic values 31 | const bookmarkedPapersWithValues = await addValuesToPapers(rawBookmarks, userID); 32 | 33 | // Response 34 | return json(bookmarkedPapersWithValues); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/main_input/input_settings.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 |
12 | {#if isAIMode} 13 | 16 |
17 | 18 |
19 | {:else if isCommentMode == false} 20 | 23 |
24 | 25 |
26 | {/if} 27 |
28 |
29 | 30 | {#if isAIMode} 31 | 32 | {:else} 33 | 34 | {/if} 35 |
36 | -------------------------------------------------------------------------------- /src/components/each_paper/summary.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
11 |
20 |
21 |
24 |
25 | 26 | Summary 27 |
28 | 29 | {paperState.paper['summary']}, 30 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/routes/api/search_papers/+server.ts: -------------------------------------------------------------------------------- 1 | // import axios from 'axios'; 2 | import { json } from '@sveltejs/kit'; 3 | import { arxivAPICall } from '../utils/search_and_clean_papers'; 4 | import { saveToDB } from '../utils/save_papers_to_db'; 5 | import { addValuesToPapers } from '../utils/add_values_to_papers'; 6 | import { getSession } from '../utils/session_manager'; 7 | 8 | export async function POST({ request }) { 9 | try { 10 | const { startIndex, maxResults, searchFilterString, sortBy, sortOrder } = await request.json(); 11 | 12 | // Get User ID 13 | const session = await getSession(request); 14 | const userID = session?.user?.id || null; // Handle case where user is not logged in 15 | 16 | let cleanedPapers = await arxivAPICall( 17 | startIndex, 18 | maxResults, 19 | searchFilterString, 20 | sortBy, 21 | sortOrder 22 | ); 23 | 24 | // Only proceed if we got papers back 25 | if (cleanedPapers && cleanedPapers.length > 0) { 26 | cleanedPapers = await addValuesToPapers(cleanedPapers, userID); 27 | await saveToDB(cleanedPapers); 28 | } 29 | 30 | return json(cleanedPapers || []); 31 | } catch (error) { 32 | console.error('Error in search_papers API:', error); 33 | return json({ error: 'Failed to fetch papers' }, { status: 500 }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export const suggestedPaperTitles: any[] = [ 3 | 'attention is all you need', 4 | 'acid', 5 | 'a theory of justice', 6 | 'augmented', 7 | 'behavioral', 8 | 'books', 9 | 'black hole', 10 | 'brain', 11 | 'cats', 12 | 'computer', 13 | 'creative', 14 | 'dog', 15 | 'dna sequencing', 16 | 'dyson sphere', 17 | 'ecg', 18 | 'emotional', 19 | 'entanglement', 20 | 'fear', 21 | 'fuzzy sets', 22 | 'fidgeting', 23 | 'glucose', 24 | 'garbage', 25 | 'gonad', 26 | 'hands', 27 | 'heart', 28 | 'higgs boson', 29 | 'hydron', 30 | 'identity', 31 | 'industrial', 32 | 'isolation', 33 | 'laptop', 34 | 'love', 35 | 'laboratory', 36 | 'machine learning', 37 | 'mathematical theory of communication', 38 | 'mental state', 39 | 'micro', 40 | 'microchip', 41 | 'mobile', 42 | 'molecular cloning', 43 | 'neural network', 44 | 'negative', 45 | 'numbers', 46 | 'pc', 47 | 'planet', 48 | 'protein measurement', 49 | 'psychology', 50 | 'quantum', 51 | 'quasar', 52 | 'qubit', 53 | 'reading', 54 | 'relationship', 55 | 'relativity', 56 | 'robotics', 57 | 'rocket', 58 | 'sitting', 59 | 'spider', 60 | 'spiritual', 61 | 'sulphur', 62 | 'television', 63 | 'tiered reward', 64 | 'transport', 65 | 'virtual reality', 66 | 'volcano', 67 | 'vision' 68 | ]; 69 | -------------------------------------------------------------------------------- /src/components/ai_chat/choose_model.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 |
11 | {aiConversationState.currentModel.model} 12 | 13 |
15 | 16 |
17 | {#each aiConversationState.models as eachModel} 18 |
19 | 20 | 21 |
(aiConversationState.currentModel = eachModel)} 24 | > 25 |
26 | {eachModel.model} 27 |
28 |
29 |
30 | {/each} 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/db/db.ts: -------------------------------------------------------------------------------- 1 | // import { MongoClient } from 'mongodb'; 2 | // // import mongoose from 'mongoose'; 3 | // import dotenv from 'dotenv'; 4 | // dotenv.config(); 5 | 6 | // export const mongoClient = new MongoClient(process.env.MONGO_URI!); 7 | // export const mongoDB = mongoClient.db('scholarxiv'); 8 | 9 | // // export const connectMongoDB = async () => { 10 | // // await mongoose.connect(process.env.MONGO_URI!); 11 | // // }; 12 | 13 | import { MongoClient } from 'mongodb'; 14 | import dotenv from 'dotenv'; 15 | dotenv.config(); 16 | 17 | declare global { 18 | var _mongoClientPromise: Promise | undefined; 19 | } 20 | 21 | if (!process.env.MONGO_URI) { 22 | throw new Error('Please define the MONGO_URI environment variable'); 23 | } 24 | 25 | const uri = process.env.MONGO_URI; 26 | const options = {}; 27 | 28 | let client; 29 | let clientPromise: Promise; 30 | 31 | if (process.env.NODE_ENV === 'development') { 32 | if (!global._mongoClientPromise) { 33 | client = new MongoClient(uri, options); 34 | global._mongoClientPromise = client.connect(); 35 | } 36 | clientPromise = global._mongoClientPromise; 37 | } else { 38 | client = new MongoClient(uri, options); 39 | clientPromise = client.connect(); 40 | } 41 | 42 | export const getDb = async () => { 43 | const client = await clientPromise; 44 | return client.db('scholarxiv'); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/skeleton/skeleton_paper.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | {#if withSummary == true} 32 |
33 | 34 |
35 | {/if} 36 |
37 | -------------------------------------------------------------------------------- /src/routes/api/save_ai_api_key/+server.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | import { json } from '@sveltejs/kit'; 3 | import { getSession } from '../utils/session_manager'; 4 | import { ObjectId } from 'mongodb'; 5 | 6 | const db = await getDb(); 7 | const user = db.collection('user'); 8 | 9 | export async function POST({ request }) { 10 | // Get User ID 11 | const session = await getSession(request); 12 | const userID = session?.user.id; 13 | 14 | const { apiKey } = await request.json(); 15 | 16 | // Return early if apiKey is empty 17 | if (!apiKey) { 18 | return json({ success: true, message: 'Empty API key, no changes made' }); 19 | } 20 | 21 | // Update API Key 22 | if (!userID) { 23 | console.error('No user ID found in session'); 24 | return json({ error: 'User not authenticated' }, { status: 401 }); 25 | } 26 | 27 | try { 28 | const result = await user.findOneAndUpdate( 29 | { _id: new ObjectId(userID) }, 30 | { $set: { apiKey } }, 31 | { returnDocument: 'after' } 32 | ); 33 | 34 | if (!result) { 35 | console.error('User not found with ID:', userID); 36 | return json({ error: 'User not found' }, { status: 404 }); 37 | } 38 | 39 | return json({ success: true, user: result }); 40 | } catch (error) { 41 | console.error('Error updating API key:', error); 42 | return json({ error: 'Failed to update API key' }, { status: 500 }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/api/utils/add_values_to_papers.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export async function addValuesToPapers(papers: any, userID: any) { 5 | const db = await getDb(); 6 | const bookmarks = db.collection('bookmarks'); 7 | const likedPapers = db.collection('likedpapers'); 8 | 9 | // User Bookmarked Papers 10 | const userBookmarkedPapers = await bookmarks 11 | .find({ userID: userID }) 12 | // .project({ paperID: 1 }) 13 | .toArray(); 14 | const bookmarkedPaperIDs = []; 15 | for (const eachBookmarkedPaper of userBookmarkedPapers) { 16 | bookmarkedPaperIDs.push(eachBookmarkedPaper.paperID); 17 | } 18 | 19 | // User Liked Papers 20 | const userLikedPapers = await likedPapers 21 | .find({ userID: userID }) 22 | // .project({ paperID: 1 }) 23 | .toArray(); 24 | const likedPaperIDs = []; 25 | for (const eachLikedPaper of userLikedPapers) { 26 | likedPaperIDs.push(eachLikedPaper.paperID); 27 | } 28 | 29 | // Is Bookmarked and Liked 30 | const papersWithValues = []; 31 | for (const eachPaper of papers) { 32 | eachPaper.isBookmarked = bookmarkedPaperIDs.includes(eachPaper.extractedID); 33 | eachPaper.isLiked = likedPaperIDs.includes(eachPaper.extractedID); 34 | eachPaper.likeCount = await likedPapers.countDocuments({ paperID: eachPaper.extractedID }); 35 | papersWithValues.push(eachPaper); 36 | } 37 | 38 | return papersWithValues; 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | ScholarXIV 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | {#if $session.data} 32 | 33 | {:else} 34 |
35 | 36 |
37 | {/if} 38 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | {#snippet children({ checked, indeterminate })} 31 | 32 | {#if indeterminate} 33 | 34 | {:else} 35 | 36 | {/if} 37 | 38 | {@render childrenProp?.()} 39 | {/snippet} 40 | 41 | -------------------------------------------------------------------------------- /src/routes/api/comment_on_paper/+server.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | import { json } from '@sveltejs/kit'; 3 | import { getSession } from '../utils/session_manager'; 4 | import { ObjectId } from 'mongodb'; 5 | 6 | // const papers = mongoDB.collection('papers'); 7 | const db = await getDb(); 8 | const users = db.collection('user'); 9 | const comments = db.collection('comments'); 10 | 11 | export async function POST({ request }) { 12 | // Get User ID 13 | const session = await getSession(request); 14 | const userID = session?.user.id; 15 | 16 | const { parentID, extractedID, comment } = await request.json(); 17 | 18 | const newComment = { 19 | userID: userID, 20 | parentID: parentID, 21 | extractedID: extractedID, 22 | comment: comment, 23 | createdAt: new Date().toISOString() 24 | }; 25 | 26 | await comments.insertOne(newComment); 27 | 28 | // Get root comments 29 | const rawComments = await comments.find({ extractedID: extractedID, parentID: null }).toArray(); 30 | 31 | // Send back updated paper 32 | // const likedPaper = [await papers.findOne({ id: paperID })]; 33 | // const updatedPaper = await addLikeValueToPapers(c, likedPaper); 34 | // Get user details for each comment 35 | const commentsWithUserInfo = await Promise.all( 36 | rawComments.map(async (comment) => { 37 | const user = await users.findOne({ _id: new ObjectId(comment.userID) }); 38 | return { 39 | ...comment, 40 | commenter: { 41 | id: user?._id.toString(), 42 | name: user?.name || 'Anonymous', 43 | email: user?.email 44 | } 45 | }; 46 | }) 47 | ); 48 | 49 | // Response 50 | return json(commentsWithUserInfo); 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { betterAuth } from 'better-auth'; 2 | import { anonymous } from 'better-auth/plugins'; 3 | import { mongodbAdapter } from 'better-auth/adapters/mongodb'; 4 | import { getDb } from '$db/db'; 5 | 6 | // Create a function to initialize auth with the database 7 | const createAuth = async () => { 8 | try { 9 | const db = await getDb(); 10 | return betterAuth({ 11 | database: mongodbAdapter(db), 12 | emailAndPassword: { 13 | enabled: true 14 | }, 15 | session: { 16 | expiresIn: 60 * 60 * 24 * 7, // 7 days 17 | updateAge: 60 * 60 * 24 // 1 day (every 1 day the session expiration is updated) 18 | }, 19 | socialProviders: { 20 | google: { 21 | clientId: process.env.GOOGLE_CLIENT_ID as string, 22 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 23 | prompt: "select_account consent" 24 | }, 25 | github: { 26 | clientId: process.env.GITHUB_CLIENT_ID as string, 27 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string 28 | }, 29 | huggingface: { 30 | clientId: process.env.HUGGINGFACE_CLIENT_ID as string, 31 | clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET as string 32 | }, 33 | twitter: { 34 | clientId: process.env.TWITTER_CLIENT_ID as string, 35 | clientSecret: process.env.TWITTER_CLIENT_SECRET as string 36 | } 37 | }, 38 | plugins: [anonymous()] 39 | }); 40 | } catch (error) { 41 | console.error('Failed to initialize auth:', error); 42 | throw error; // This will prevent the app from starting if auth initialization fails 43 | } 44 | }; 45 | 46 | // Initialize auth and export it 47 | export const auth = createAuth(); 48 | -------------------------------------------------------------------------------- /src/routes/api/get_paper_comments/+server.ts: -------------------------------------------------------------------------------- 1 | // import axios from 'axios'; 2 | import { json } from '@sveltejs/kit'; 3 | import { ObjectId } from 'mongodb'; 4 | import { addValuesToPapers } from '../utils/add_values_to_papers'; 5 | import { getSession } from '../utils/session_manager'; 6 | import { getDb } from '$db/db'; 7 | 8 | // const bookmarks = mongoDB.collection('bookmarks'); 9 | 10 | const db = await getDb(); 11 | const papers = db.collection('papers'); 12 | const comments = db.collection('comments'); 13 | const users = db.collection('user'); 14 | 15 | export async function POST({ request }) { 16 | const { extractedID } = await request.json(); 17 | 18 | // Get User ID 19 | const session = await getSession(request); 20 | const userID = session?.user.id; 21 | 22 | // Get Paper Comments 23 | const rawPaper = [await papers.findOne({ extractedID: extractedID })]; 24 | const paperWithDynamicValues = await addValuesToPapers(rawPaper, userID); 25 | 26 | // Get root comments 27 | const rawComments = await comments.find({ extractedID: extractedID, parentID: null }).toArray(); 28 | 29 | // Get user details for each comment 30 | const commentsWithUserInfo = await Promise.all( 31 | rawComments.map(async (comment) => { 32 | const user = await users.findOne({ _id: new ObjectId(comment.userID) }); 33 | return { 34 | ...comment, 35 | commenter: { 36 | id: user?._id.toString(), 37 | name: user?.name || 'Anonymous', 38 | email: user?.email 39 | } 40 | }; 41 | }) 42 | ); 43 | 44 | const result = { 45 | paper: paperWithDynamicValues[0], 46 | comments: commentsWithUserInfo 47 | }; 48 | 49 | return json(result); 50 | } 51 | -------------------------------------------------------------------------------- /src/routes/api/bookmark_papers/+server.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from '$db/db'; 2 | import { json } from '@sveltejs/kit'; 3 | import { getSession } from '../utils/session_manager'; 4 | 5 | const db = await getDb(); 6 | const papers = db.collection('papers'); 7 | const bookmarks = db.collection('bookmarks'); 8 | 9 | export async function POST({ request }) { 10 | // Get User ID 11 | const session = await getSession(request); 12 | const userID = session?.user.id; 13 | 14 | const { paperID } = await request.json(); 15 | 16 | // New bookmark obj 17 | const newBookmark = { 18 | userID: userID, 19 | paperID: paperID, 20 | createdAt: new Date().toISOString() 21 | }; 22 | 23 | // Add new bookmark 24 | const existingBookmark = await bookmarks.findOne({ userID: userID, paperID: paperID }); 25 | if (existingBookmark) { 26 | // Delete the existing bookmark 27 | await bookmarks.deleteOne({ userID: userID, paperID: paperID }); 28 | } else { 29 | await bookmarks.insertOne(newBookmark); 30 | } 31 | 32 | // Send back all bookmarks 33 | const bookmarkedPapers = await getBookmarkedPapers(userID!); 34 | 35 | // Response 36 | return json(bookmarkedPapers); 37 | } 38 | 39 | async function getBookmarkedPapers(userID: string) { 40 | // Get User Bookmarks 41 | const result = await bookmarks.find({ userID: userID }).toArray(); 42 | 43 | // Extract paperIDs from the bookmarks 44 | const paperIDs = result.map((bookmark) => bookmark.paperID); 45 | 46 | // Fetch papers using the extracted paperIDs 47 | const rawBookmarks = await papers.find({ id: { $in: paperIDs } }).toArray(); 48 | 49 | // Add dynamic values 50 | // const bookmarkedPapers = await addDynamicValuesToPapers(c, rawBookmarks); 51 | 52 | return rawBookmarks; 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 29 | 30 | 35 | {@render children?.()} 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 30 | {@render children?.()} 31 | 34 | 35 | Close 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/select_all.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 |
0 26 | ? 'flex h-7 w-14 items-center justify-center rounded-full px-1 hover:bg-zinc-100' 27 | : 'flex h-7 w-14 items-center justify-center rounded-full p-1 hover:bg-zinc-100'} 28 | onclick={() => selectAll()} 29 | > 30 |
0 32 | ? 'px-1 pb-1 font-semibold text-emerald-500' 33 | : 'px-1 pb-1 font-semibold text-zinc-400'} 34 | > 35 | {aiConversationState.selectedPapersList.length} 36 |
37 | 0 42 | ? 'cursor-pointer text-emerald-500' 43 | : 'cursor-pointer'} 44 | /> 45 |
46 | -------------------------------------------------------------------------------- /src/lib/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; 2 | import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; 3 | import Content from "./dropdown-menu-content.svelte"; 4 | import GroupHeading from "./dropdown-menu-group-heading.svelte"; 5 | import Item from "./dropdown-menu-item.svelte"; 6 | import Label from "./dropdown-menu-label.svelte"; 7 | import RadioItem from "./dropdown-menu-radio-item.svelte"; 8 | import Separator from "./dropdown-menu-separator.svelte"; 9 | import Shortcut from "./dropdown-menu-shortcut.svelte"; 10 | import SubContent from "./dropdown-menu-sub-content.svelte"; 11 | import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; 12 | 13 | const Sub = DropdownMenuPrimitive.Sub; 14 | const Root = DropdownMenuPrimitive.Root; 15 | const Trigger = DropdownMenuPrimitive.Trigger; 16 | const Group = DropdownMenuPrimitive.Group; 17 | const RadioGroup = DropdownMenuPrimitive.RadioGroup; 18 | 19 | export { 20 | CheckboxItem, 21 | Content, 22 | Root as DropdownMenu, 23 | CheckboxItem as DropdownMenuCheckboxItem, 24 | Content as DropdownMenuContent, 25 | Group as DropdownMenuGroup, 26 | GroupHeading as DropdownMenuGroupHeading, 27 | Item as DropdownMenuItem, 28 | Label as DropdownMenuLabel, 29 | RadioGroup as DropdownMenuRadioGroup, 30 | RadioItem as DropdownMenuRadioItem, 31 | Separator as DropdownMenuSeparator, 32 | Shortcut as DropdownMenuShortcut, 33 | Sub as DropdownMenuSub, 34 | SubContent as DropdownMenuSubContent, 35 | SubTrigger as DropdownMenuSubTrigger, 36 | Trigger as DropdownMenuTrigger, 37 | Group, 38 | GroupHeading, 39 | Item, 40 | Label, 41 | RadioGroup, 42 | RadioItem, 43 | Root, 44 | Separator, 45 | Shortcut, 46 | Sub, 47 | SubContent, 48 | SubTrigger, 49 | Trigger, 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xivweb", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "engines": { 7 | "node": ">=18 <21" 8 | }, 9 | "scripts": { 10 | "dev": "vite dev", 11 | "build": "vite build", 12 | "preview": "vite preview", 13 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 14 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 15 | "format": "prettier --write .", 16 | "lint": "prettier --check . && eslint ." 17 | }, 18 | "devDependencies": { 19 | "@eslint/compat": "^1.2.3", 20 | "@eslint/js": "^9.17.0", 21 | "@sveltejs/adapter-auto": "^3.0.0", 22 | "@sveltejs/kit": "^2.0.0", 23 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 24 | "@types/node": "^22.10.5", 25 | "autoprefixer": "^10.4.20", 26 | "bits-ui": "^1.1.0", 27 | "clsx": "^2.1.1", 28 | "eslint": "^9.7.0", 29 | "eslint-config-prettier": "^9.1.0", 30 | "eslint-plugin-svelte": "^2.36.0", 31 | "globals": "^15.0.0", 32 | "lucide-svelte": "^0.473.0", 33 | "mode-watcher": "^0.5.0", 34 | "prettier": "^3.3.2", 35 | "prettier-plugin-svelte": "^3.2.6", 36 | "prettier-plugin-tailwindcss": "^0.6.5", 37 | "svelte": "^5.0.0", 38 | "svelte-check": "^4.0.0", 39 | "svelte-sonner": "^0.3.28", 40 | "tailwind-merge": "^2.6.0", 41 | "tailwind-variants": "^0.3.0", 42 | "tailwindcss": "^3.4.9", 43 | "typescript": "^5.0.0", 44 | "typescript-eslint": "^8.0.0", 45 | "vite": "^5.4.11" 46 | }, 47 | "dependencies": { 48 | "axios": "^1.7.9", 49 | "better-auth": "^1.2.12", 50 | "dotenv": "^16.4.7", 51 | "fast-xml-parser": "^4.5.1", 52 | "moment": "^2.30.1", 53 | "mongoose": "^8.9.4", 54 | "openai": "^4.83.0", 55 | "svelte-exmarkdown": "^4.0.2", 56 | "svelte-loading-spinners": "^0.3.6" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import { cubicOut } from "svelte/easing"; 4 | import type { TransitionConfig } from "svelte/transition"; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | type FlyAndScaleParams = { 11 | y?: number; 12 | x?: number; 13 | start?: number; 14 | duration?: number; 15 | }; 16 | 17 | export const flyAndScale = ( 18 | node: Element, 19 | params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } 20 | ): TransitionConfig => { 21 | const style = getComputedStyle(node); 22 | const transform = style.transform === "none" ? "" : style.transform; 23 | 24 | const scaleConversion = ( 25 | valueA: number, 26 | scaleA: [number, number], 27 | scaleB: [number, number] 28 | ) => { 29 | const [minA, maxA] = scaleA; 30 | const [minB, maxB] = scaleB; 31 | 32 | const percentage = (valueA - minA) / (maxA - minA); 33 | const valueB = percentage * (maxB - minB) + minB; 34 | 35 | return valueB; 36 | }; 37 | 38 | const styleToString = ( 39 | style: Record 40 | ): string => { 41 | return Object.keys(style).reduce((str, key) => { 42 | if (style[key] === undefined) return str; 43 | return str + `${key}:${style[key]};`; 44 | }, ""); 45 | }; 46 | 47 | return { 48 | duration: params.duration ?? 200, 49 | delay: 0, 50 | css: (t) => { 51 | const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); 52 | const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); 53 | const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); 54 | 55 | return styleToString({ 56 | transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, 57 | opacity: t 58 | }); 59 | }, 60 | easing: cubicOut 61 | }; 62 | }; -------------------------------------------------------------------------------- /src/lib/auth_functions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { goto } from '$app/navigation'; 3 | import { signIn, signUp, signOut, forgetPassword, resetPassword } from './auth_client'; 4 | 5 | export const handleSignIn = async (email: string, password: string) => { 6 | await signIn.email( 7 | { 8 | email: email, 9 | password: password 10 | }, 11 | { 12 | onError(context: any) { 13 | console.log(context); 14 | }, 15 | onSuccess() { 16 | goto('/homepage'); 17 | } 18 | } 19 | ); 20 | }; 21 | 22 | export const handleSignUp = async ( 23 | firstName: string, 24 | lastName: string, 25 | email: string, 26 | password: string 27 | ) => { 28 | await signUp.email({ 29 | email: email, 30 | password: password, 31 | name: `${firstName} ${lastName}`, 32 | fetchOptions: { 33 | onError(context: any) { 34 | alert(context.error.message); 35 | }, 36 | onSuccess() { 37 | goto('/homepage'); 38 | } 39 | } 40 | }); 41 | }; 42 | 43 | export const handleSignOut = async () => { 44 | await signOut({ 45 | fetchOptions: { 46 | onSuccess() { 47 | goto('/'); 48 | }, 49 | onError(context: any) { 50 | alert(context.error.message); 51 | } 52 | } 53 | }); 54 | }; 55 | 56 | export const handleForgetPassword = async (email: string) => { 57 | await forgetPassword( 58 | { 59 | email: email, 60 | redirectTo: '/auth/reset_password' 61 | }, 62 | { 63 | onSuccess() { 64 | alert('Password reset link sent to your email'); 65 | goto('/'); 66 | }, 67 | onError(context: any) { 68 | alert(context.error.message); 69 | } 70 | } 71 | ); 72 | }; 73 | 74 | export const handleResetPassword = async (password: string) => { 75 | await resetPassword({ 76 | newPassword: password, 77 | fetchOptions: { 78 | onSuccess() { 79 | window.location.href = '/auth/sign_in'; 80 | }, 81 | onError(context: any) { 82 | alert(context.error.message); 83 | } 84 | } 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/title.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {#if showTitle == true} 7 | {#if useAsHome == true} 8 | ScholarXIV 9 | {:else} 10 | ScholarXIV 11 | {/if} 12 | {/if} 13 | 14 | {#if showDescription == true} 15 | 16 | {#if useAsHome == true} 17 | 18 | Explore academic papers 19 | 26 | 27 | {:else} 28 | 29 | Explore academic papers from the arXiv repository. 34 | 35 | {/if} 36 | 37 | 38 | {#if useAsHome == true} 39 |
40 | 43 | AI powered and fully 44 | open-source. 49 | 50 | {:else} 51 | 52 | AI powered and fully 53 | open-source. 58 | 59 | {/if} 60 |
61 | {/if} 62 |
63 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: 240 5.9% 10%; 30 | 31 | --destructive: 0 72.2% 50.6%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 10% 3.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: 0 0% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 0% 98%; 66 | 67 | --ring: 240 4.9% 83.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | 80 | @layer utilities { 81 | /* Hide scrollbar for Chrome, Safari, and Opera */ 82 | .no-scrollbar::-webkit-scrollbar { 83 | display: none; 84 | } 85 | /* Hide scrollbar for IE, Edge, and Firefox */ 86 | .no-scrollbar { 87 | -ms-overflow-style: none; /* IE and Edge */ 88 | scrollbar-width: none; /* Firefox */ 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | import type { Config } from "tailwindcss"; 3 | 4 | const config: Config = { 5 | darkMode: ["class"], 6 | content: ["./src/**/*.{html,js,svelte,ts}"], 7 | safelist: ["dark"], 8 | theme: { 9 | container: { 10 | center: true, 11 | padding: "2rem", 12 | screens: { 13 | "2xl": "1400px" 14 | } 15 | }, 16 | extend: { 17 | colors: { 18 | border: "hsl(var(--border) / )", 19 | input: "hsl(var(--input) / )", 20 | ring: "hsl(var(--ring) / )", 21 | background: "hsl(var(--background) / )", 22 | foreground: "hsl(var(--foreground) / )", 23 | primary: { 24 | DEFAULT: "hsl(var(--primary) / )", 25 | foreground: "hsl(var(--primary-foreground) / )" 26 | }, 27 | secondary: { 28 | DEFAULT: "hsl(var(--secondary) / )", 29 | foreground: "hsl(var(--secondary-foreground) / )" 30 | }, 31 | destructive: { 32 | DEFAULT: "hsl(var(--destructive) / )", 33 | foreground: "hsl(var(--destructive-foreground) / )" 34 | }, 35 | muted: { 36 | DEFAULT: "hsl(var(--muted) / )", 37 | foreground: "hsl(var(--muted-foreground) / )" 38 | }, 39 | accent: { 40 | DEFAULT: "hsl(var(--accent) / )", 41 | foreground: "hsl(var(--accent-foreground) / )" 42 | }, 43 | popover: { 44 | DEFAULT: "hsl(var(--popover) / )", 45 | foreground: "hsl(var(--popover-foreground) / )" 46 | }, 47 | card: { 48 | DEFAULT: "hsl(var(--card) / )", 49 | foreground: "hsl(var(--card-foreground) / )" 50 | } 51 | }, 52 | borderRadius: { 53 | lg: "var(--radius)", 54 | md: "calc(var(--radius) - 2px)", 55 | sm: "calc(var(--radius) - 4px)" 56 | }, 57 | fontFamily: { 58 | sans: [...fontFamily.sans] 59 | } 60 | } 61 | }, 62 | }; 63 | 64 | export default config; 65 | -------------------------------------------------------------------------------- /src/routes/api/delete_comment/+server.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import { json } from '@sveltejs/kit'; 3 | import { getSession } from '../utils/session_manager'; 4 | import { getDb } from '$db/db'; 5 | 6 | const db = await getDb(); 7 | const users = db.collection('user'); 8 | const comments = db.collection('comments'); 9 | 10 | export async function DELETE({ request }) { 11 | try { 12 | // Get User ID from session 13 | const session = await getSession(request); 14 | if (!session?.user?.id) { 15 | return json({ error: 'Unauthorized' }, { status: 401 }); 16 | } 17 | const userID = session.user.id; 18 | 19 | // Get comment ID from request body 20 | const { commentId, extractedID } = await request.json(); 21 | if (!commentId || !extractedID) { 22 | return json({ error: 'Missing required fields' }, { status: 400 }); 23 | } 24 | 25 | // Verify the comment exists and belongs to the user 26 | const comment = await comments.findOne({ 27 | _id: new ObjectId(commentId), 28 | userID: userID 29 | }); 30 | 31 | if (!comment) { 32 | return json({ error: 'Comment not found or access denied' }, { status: 404 }); 33 | } 34 | 35 | // Delete the comment 36 | await comments.deleteOne({ _id: new ObjectId(commentId) }); 37 | 38 | // Get updated comments for the paper 39 | const rawComments = await comments 40 | .find({ 41 | extractedID: extractedID, 42 | parentID: null 43 | }) 44 | .toArray(); 45 | 46 | // Get user details for each comment 47 | const commentsWithUserInfo = await Promise.all( 48 | rawComments.map(async (comment) => { 49 | const user = await users.findOne({ _id: new ObjectId(comment.userID) }); 50 | return { 51 | ...comment, 52 | commenter: { 53 | id: user?._id.toString(), 54 | name: user?.name || 'Anonymous', 55 | email: user?.email 56 | } 57 | }; 58 | }) 59 | ); 60 | 61 | return json(commentsWithUserInfo); 62 | } catch (error) { 63 | console.error('Error deleting comment:', error); 64 | return json({ error: 'Internal server error' }, { status: 500 }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/ai_chat/selected_papers.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 |
0 20 | ? 'flex cursor-pointer items-center gap-x-2 text-xs text-emerald-600 hover:text-black' 21 | : 'flex cursor-pointer items-center gap-x-2 text-xs text-zinc-500 hover:text-black'} 22 | > 23 | 26 |
27 | 28 |
29 | 30 | {aiConversationState.selectedPapersList.length} papers selected 31 |
33 | {#if aiConversationState.selectedPapersList.length > 0} 34 | 35 |
36 |
37 | {#each aiConversationState.selectedPapersList as eachSelectedPaper} 38 |
41 |
42 | {eachSelectedPaper['title']} 43 |
44 | 45 | 46 |
unselectPaper(eachSelectedPaper)} 49 | > 50 | 51 |
52 |
53 | {/each} 54 |
55 |
56 |
57 | {/if} 58 |
59 | -------------------------------------------------------------------------------- /src/routes/api/ai_chat/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import OpenAI from 'openai'; 3 | import { getDb } from '$db/db'; 4 | import { getSession } from '../utils/session_manager'; 5 | import { ObjectId } from 'mongodb'; 6 | 7 | const aiSystemPrompt = 8 | 'You are a research assistant helping people navigate and understand research papers more. You are inside an arxiv repository and the users will often send you a list of papers they have selected along with your previous conversation history so based on these try your best to be helpful. Do not flat out spill the conversation context or the raw selected papers data. Sometimes you will be given empty lists of conversation history or selected papers so just ignore those. Other than that try to be smart, be precise, helpful and make things simpler to understand. Donot use emojis alot.'; 9 | 10 | const db = await getDb(); 11 | const user = db.collection('user'); 12 | 13 | export async function POST({ request }) { 14 | // Get user session 15 | const session = await getSession(request); 16 | const userID = session?.user.id; 17 | 18 | if (!userID) { 19 | return json({ error: 'User not authenticated' }, { status: 401 }); 20 | } 21 | 22 | // Get user's API key from database 23 | const userDoc = await user.findOne({ _id: new ObjectId(userID) }); 24 | if (!userDoc?.apiKey) { 25 | aiResponse = 26 | 'No API key found. Please set your API key by clicking on the settings icon in the main input box below.'; 27 | return json(aiResponse); 28 | } 29 | 30 | const { selectedPapers, conversation, prompt } = await request.json(); 31 | 32 | const openAI = new OpenAI({ 33 | apiKey: userDoc.apiKey, 34 | baseURL: 'https://generativelanguage.googleapis.com/v1beta/' 35 | }); 36 | 37 | // Result 38 | var aiResponse; 39 | try { 40 | const result = await openAI.chat.completions.create({ 41 | model: 'gemini-2.0-flash', 42 | messages: [ 43 | { role: 'system', content: aiSystemPrompt }, 44 | { 45 | role: 'user', 46 | content: 47 | prompt + 48 | 'Selectd Papers are: ' + 49 | selectedPapers + 50 | 'Previous Conversation History is: ' + 51 | conversation 52 | } 53 | ] 54 | }); 55 | aiResponse = result.choices[0].message.content; 56 | } catch (error) { 57 | // console.error('Error:', error); 58 | aiResponse = 59 | 'Invalid API Key please check your API Key by clicking on the settings icon in the main input box below.'; 60 | } 61 | 62 | return json(aiResponse); 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | 55 | 56 | {#if href} 57 | 63 | {@render children?.()} 64 | 65 | {:else} 66 | 74 | {/if} 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Code of Conduct 2 | Our Pledge 3 | 4 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 5 | Our Standards 6 | 7 | Examples of behavior that contributes to creating a positive environment include: 8 | 9 | Using welcoming and inclusive language 10 | Being respectful of differing viewpoints and experiences 11 | Gracefully accepting constructive criticism 12 | Focusing on what is best for the community 13 | Showing empathy towards other community members 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | The use of sexualized language or imagery and unwelcome sexual attention or advances 18 | Trolling, insulting/derogatory comments, and personal or political attacks 19 | Public or private harassment 20 | Publishing others' private information, such as a physical or electronic address, without explicit permission 21 | Other conduct which could reasonably be considered inappropriate in a professional setting 22 | 23 | Our Responsibilities 24 | 25 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 26 | 27 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 28 | Scope 29 | 30 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 31 | 32 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 33 | Attribution 34 | 35 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 36 | -------------------------------------------------------------------------------- /src/state/each_paper_state.svelte.ts: -------------------------------------------------------------------------------- 1 | // import { mongoDB } from '$db/db'; 2 | import { paperListState } from './papers_list.svelte'; 3 | 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | export class EachPaper { 6 | paper: any = $state(); 7 | likeCount: number = $state(0); 8 | commentCount: number = $state(0); 9 | isLiked: boolean = $state(false); 10 | isBookmarked: boolean = $state(false); 11 | isReadingSummary: boolean = $state(false); 12 | isFirstInList: boolean = $state(false); 13 | 14 | constructor(paper: any) { 15 | this.paper = paper; 16 | this.likeCount = this.paper.likeCount || 0; 17 | this.commentCount = this.paper['commentCount'] || 0; 18 | this.isLiked = this.paper.isLiked; 19 | this.isBookmarked = this.paper.isBookmarked; 20 | // this.isReadingSummary = paperListState.paperList[0]['extractedID'] == this.paper['extractedID']; 21 | this.isFirstInList = paperListState.paperList[0]['extractedID'] == this.paper['extractedID']; 22 | } 23 | 24 | async toggleLike(userID: any, paperID: any) { 25 | if (this.isLiked == true) { 26 | // this.likes -= 1; 27 | this.likeCount = 0; 28 | } else { 29 | this.likeCount += 1; 30 | } 31 | 32 | this.isLiked = !this.isLiked; 33 | 34 | // Sync Bookmark State of Main Feed 35 | for (const eachPaper of paperListState.paperList as any[]) { 36 | if (eachPaper['extractedID'] == paperID) { 37 | eachPaper['isLiked'] = this.isLiked; 38 | eachPaper['likeCount'] = this.likeCount; 39 | } 40 | } 41 | 42 | await fetch('/api/like_papers', { 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/json' 46 | }, 47 | body: JSON.stringify({ paperID }) 48 | }); 49 | } 50 | 51 | async toggleBookmark(userID: any, paperID: any) { 52 | this.isBookmarked = !this.isBookmarked; 53 | 54 | // Sync Bookmark State of Main Feed 55 | for (const eachPaper of paperListState.paperList as any[]) { 56 | if (eachPaper['extractedID'] == paperID) { 57 | eachPaper['isBookmarked'] = this.isBookmarked; 58 | } 59 | } 60 | 61 | // Bookmark Paper 62 | await fetch('/api/bookmark_papers', { 63 | method: 'POST', 64 | headers: { 65 | 'Content-Type': 'application/json' 66 | }, 67 | body: JSON.stringify({ paperID }) 68 | }); 69 | 70 | paperListState.isGettingBookmarkedPapers = false; 71 | 72 | // Send back all bookmarks 73 | // const bookmarkedPapers = await getBookmarkedPapers(c, userID); 74 | // paperListState.bookmarkList = newBookmarkList.body.json(); 75 | 76 | // Response 77 | // return c.json(bookmarkedPapers); 78 | } 79 | 80 | toggleSummary() { 81 | if (this.isFirstInList) { 82 | this.isFirstInList = false; 83 | } else { 84 | this.isReadingSummary = !this.isReadingSummary; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/routes/liked_papers_page/+page.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | ScholarXIV | Likes 33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |
Liked Papers
45 | 46 | 47 | 51 |
52 | 53 | 54 |
55 | {#if paperListState.isGettingLikedPapers == true} 56 | 57 | {:else} 58 | {#each paperListState.likedPapersList as eachPaper} 59 | 60 | {/each} 61 | {/if} 62 |
63 |
64 | 65 | 66 |
67 | 68 | 69 |
70 | Showing {paperListState.likedPapersList.length} Papers. 71 |
72 | 73 | 74 |
75 |
76 |
77 | 78 | 79 |
80 | 81 |
82 | 85 |
86 | -------------------------------------------------------------------------------- /src/routes/bookmarks_page/+page.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | ScholarXIV | Bookmarks 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 |
Bookmarked Papers
46 | 47 | 48 | 52 |
53 | 54 | 55 |
56 | {#if paperListState.isGettingBookmarkedPapers == true} 57 | 58 | {:else} 59 | {#each paperListState.bookmarkList as eachPaper} 60 | 61 | {/each} 62 | {/if} 63 |
64 |
65 | 66 | 67 |
68 | 69 | 70 |
71 | Showing {paperListState.bookmarkList.length} Papers. 72 |
73 | 74 | 75 |
76 |
77 |
78 | 79 | 80 |
81 | 82 |
83 | 86 |
87 | -------------------------------------------------------------------------------- /src/components/each_paper/each_paper.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | 35 | 36 |
selectPaper()}> 37 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 |
63 |
64 | -------------------------------------------------------------------------------- /src/routes/comments/papers/[paperid]/+page.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | ScholarXIV | Comments 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 |
42 | {#if commentState.isGettingComments == true} 43 | 44 | {:else} 45 | 46 | {/if} 47 |
48 | 49 | 50 |
51 | 52 |
53 | 54 |
Comments
55 |
56 | 57 | 58 |
59 | {#if commentState.isGettingComments == true} 60 | 61 | {:else} 62 | {#each commentState.comments as eachComment (eachComment._id)} 63 | { 66 | // Remove the deleted comment from the state 67 | commentState.comments = commentState.comments.filter( 68 | (c) => c._id !== e.detail.commentId 69 | ); 70 | }} 71 | /> 72 | {/each} 73 | {/if} 74 |
75 |
76 | 77 | 78 |
79 | 80 | 81 |
82 | Showing {commentState.comments.length} Comments. 83 |
84 | 85 | 86 |
87 | 88 |
89 | 92 |
93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ScholarXIV](https://scholarxiv.com) 2 | 3 | A modern, AI-powered academic research platform that helps you discover, analyze, and interact with research papers more efficiently. 4 | 5 | ![Homepage](/static/screenshots/screenshot.jpg) 6 | 7 | ## Features 8 | 9 | - **Smart Search** - Find relevant papers with powerful search capabilities 10 | - **Advanced Filters** - Refine your search with advanced filtering options 11 | - **Context-Aware Chat** - Chat with an AI assistant about research papers 12 | - **Multi-Paper Analysis** - Select and compare multiple papers in a single chat session 13 | - **Save & Organize** - Like and bookmark papers for later reference 14 | - **Comment** - comment and view discussions about papers 15 | - **Collections** - View and manage your liked and bookmarked papers 16 | - **In-Browser Viewing** - Read papers directly in your browser 17 | - **Download Options** - Download papers in PDF format 18 | 19 | ### Technologies Used 20 | 21 | - **Frontend**: SvelteKit, TypeScript, Tailwind CSS 22 | - **Backend**: MongoDB 23 | - **AI**: Gemini 24 | 25 | ### Self Hosting Guide 26 | 27 | 1. Clone the repository 28 | 2. Install dependencies: `npm install` 29 | 3. Set up environment variables (refer to `.env.example`) 30 | 4. Run the development server: `npm run dev` 31 | 5. Open [http://localhost:5173](http://localhost:5173) in your browser 32 | 33 | ### How to Contribute 34 | 35 | We welcome contributions from the community! Follow these steps to contribute: 36 | 37 | 1. **Fork the Repository**: Fork the ScholarXIV repository to your GitHub account. 38 | 2. **Make Changes**: Create a new branch, make your changes, and commit them to your branch. 39 | 3. **Create a Pull Request**: Once your changes are ready, create a detailed pull request (PR) explaining the changes you've made. 40 | 4. **Review and Iterate**: Collaborate with the maintainers to review and iterate on your changes until they are ready to be merged. 41 | 5. **That's it!**: Can't wait to see the wonders you'll be doing. 42 | 43 | For more detailed guidelines on contributing, please refer to our contribution guidelines. 44 | 45 | ## Screenshots 46 | 47 | | | | 48 | | ---------------------------------------- | ---------------------------------------- | 49 | | ![](static/screenshots/screenshot1.jpg) | ![](static/screenshots/screenshot2.jpg) | 50 | | ![](static/screenshots/screenshot3.jpg) | ![](static/screenshots/screenshot4.jpg) | 51 | | ![](static/screenshots/screenshot5.jpg) | ![](static/screenshots/screenshot6.jpg) | 52 | | ![](static/screenshots/screenshot7.jpg) | ![](static/screenshots/screenshot8.jpg) | 53 | | ![](static/screenshots/screenshot9.jpg) | ![](static/screenshots/screenshot10.jpg) | 54 | | ![](static/screenshots/screenshot11.jpg) | ![](static/screenshots/screenshot12.jpg) | 55 | 56 | ## License 57 | 58 | This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details. 59 | 60 | ## Contributing 61 | 62 | Contributions are welcome! Please feel free to submit a Pull Request. 63 | 64 | ## Contact 65 | 66 | Have questions or suggestions? Feel free to open an issue or reach out to our team. 67 | -------------------------------------------------------------------------------- /src/components/main_input/ai_settings.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 | 58 | 59 | API Key 60 | Enter your Gemini API Key 61 | 62 | 69 | {#if showEmptyError} 70 |

Please enter an API key

71 | {/if} 72 | 73 | 74 |
75 | {#if isSavingAPIKey == true} 76 |
77 |
80 | 81 |
82 |
83 | {:else} 84 | 85 | 86 |
saveAPIKey()} 89 | > 90 | Save API Key 91 |
92 | {/if} 93 | 94 | 95 | 96 |
99 | Get API Key 100 |
101 |
102 |
103 |
104 |
105 | -------------------------------------------------------------------------------- /src/components/navigation.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 | 20 | 21 | <!-- Navigation Buttons and Profile --> 22 | 23 | <div class="flex items-start justify-center gap-x-3"> 24 | <!-- Navigation Buttons --> 25 | <div class="flex items-start justify-center gap-x-3"> 26 | {#if page.url.pathname == '/homepage'} 27 | {#if $session.data} 28 | <div class="flex"> 29 | <!-- Liked Papers --> 30 | <NavigationButtons icon={Heart} size={16} link={'/liked_papers_page'} /> 31 | <!-- Bookmarks --> 32 | <NavigationButtons icon={Bookmark} size={16} link={'/bookmarks_page'} /> 33 | </div> 34 | {/if} 35 | {:else if page.url.pathname == '/bookmarks_page'} 36 | <div class="flex"> 37 | {#if $session.data} 38 | <!-- Discover --> 39 | <NavigationButtons icon={Compass} size={18} link={'/homepage'} /> 40 | {/if} 41 | <!-- Liked Papers --> 42 | <NavigationButtons icon={Heart} size={16} link={'/liked_papers_page'} /> 43 | </div> 44 | {:else if page.url.pathname == '/liked_papers_page'} 45 | <div class="flex"> 46 | {#if $session.data} 47 | <!-- Discover --> 48 | <NavigationButtons icon={Compass} size={18} link={'/homepage'} /> 49 | {/if} 50 | <!-- Bookmarks --> 51 | <NavigationButtons icon={Bookmark} size={16} link={'/bookmarks_page'} /> 52 | </div> 53 | {:else} 54 | <div class="flex gap-x-2"> 55 | {#if $session.data} 56 | <!-- Discover --> 57 | <NavigationButtons icon={Compass} size={18} link={'/homepage'} /> 58 | {/if} 59 | 60 | <!-- Liked Papers --> 61 | <NavigationButtons icon={Heart} size={16} link={'/liked_papers_page'} /> 62 | 63 | <!-- Bookmarks --> 64 | <NavigationButtons icon={Bookmark} size={16} link={'/bookmarks_page'} /> 65 | </div> 66 | {/if} 67 | </div> 68 | 69 | {#if $session.data} 70 | <!-- Profile --> 71 | <div class="pr-2 pt-2"> 72 | {#if $session.data} 73 | <DropdownMenu.Root> 74 | <DropdownMenu.Trigger> 75 | <ProfileAvatar session={$session} /> 76 | </DropdownMenu.Trigger> 77 | <DropdownMenu.Content> 78 | <DropdownMenu.Group> 79 | <!-- Profile --> 80 | <DropdownMenu.Item 81 | ><ProfileAvatar session={$session} fullInfo={true} /></DropdownMenu.Item 82 | > 83 | 84 | <div class="py-1"> 85 | <Separator /> 86 | </div> 87 | <!-- Logout --> 88 | <!-- svelte-ignore a11y-click-events-have-key-events --> 89 | <!-- svelte-ignore a11y-no-static-element-interactions --> 90 | <DropdownMenu.Item 91 | ><div 92 | class="w-full cursor-pointer text-center hover:text-red-500" 93 | onclick={() => handleSignOut()} 94 | > 95 | Logout 96 | </div></DropdownMenu.Item 97 | > 98 | </DropdownMenu.Group> 99 | </DropdownMenu.Content> 100 | </DropdownMenu.Root> 101 | {/if} 102 | </div> 103 | {/if} 104 | </div> 105 | </div> 106 | -------------------------------------------------------------------------------- /src/routes/api/utils/search_and_clean_papers.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { XMLParser } from 'fast-xml-parser'; 3 | import { 4 | baseURL, 5 | defaultMaxResults, 6 | defaultSortBy, 7 | defaultSortOrder, 8 | defaultStartIndex, 9 | pdfBaseURL 10 | } from '../constants'; 11 | import type { searchStringOBJI } from '../types/types'; 12 | 13 | // await axios.get(`${baseURL}${searchFilterString}&start=${startIndex}&max_results=${maxResults}&sortBy=${sortBy}&sortOrder=${sortOrder}`); 14 | 15 | export async function arxivAPICall( 16 | startIndex = defaultStartIndex, 17 | maxResults = defaultMaxResults, 18 | searchFilterString: searchStringOBJI, 19 | sortBy = defaultSortBy, 20 | sortOrder = defaultSortOrder 21 | ) { 22 | const createdSearchString = createSearchString(searchFilterString); 23 | 24 | const responseXML = await axios.get( 25 | `${baseURL}${createdSearchString}&start=${startIndex}&max_results=${maxResults}&sortBy=${sortBy}&sortOrder=${sortOrder}` 26 | ); 27 | 28 | const jsObj = parseXMLToJS(responseXML.data); 29 | const rawPapers = jsObj['feed']['entry'] || []; 30 | 31 | const cleanedPapers = await cleanPapers(rawPapers); 32 | 33 | return cleanedPapers; 34 | } 35 | 36 | // Create the filtering search string connected with AND 37 | function createSearchString(searchFilter: searchStringOBJI) { 38 | // Check if the 'all' key has a value, return it if so 39 | if (searchFilter.all) { 40 | return `all:${searchFilter.all}`; 41 | } 42 | 43 | const searchParams = []; 44 | 45 | // Iterate over other keys only if 'all' is not present or empty 46 | for (const key in searchFilter) { 47 | if (key !== 'all' && searchFilter[key as keyof typeof searchFilter]) { 48 | // Skip 'all' key 49 | searchParams.push(`${key}:${searchFilter[key as keyof typeof searchFilter]}`); 50 | } 51 | } 52 | 53 | return searchParams.join('+AND+'); 54 | } 55 | 56 | // Function to parse XLM to JS object 57 | export function parseXMLToJS(data: string) { 58 | const parser = new XMLParser(); 59 | const jsObj = parser.parse(data); 60 | return jsObj; 61 | } 62 | 63 | // Function to Identify PDF link 64 | export function parsePDFLinkFromPaperID(paperID: string) { 65 | const extractedID = paperID.split('/').pop(); 66 | let pdfURL = ''; 67 | if (extractedID && extractedID.includes('.')) { 68 | pdfURL = `${pdfBaseURL}/${extractedID}`; 69 | } else { 70 | pdfURL = `${pdfBaseURL}/cond-mat/${extractedID}`; 71 | } 72 | return pdfURL; 73 | } 74 | 75 | // Function to remove /n from texts 76 | export function removeNewLineCharacter(text: string) { 77 | const cleanedText = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); 78 | return cleanedText; 79 | } 80 | 81 | // Funtion to clear the response 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | export async function cleanPapers(rawPapers: any) { 84 | const cleanedPapers = []; 85 | for (const eachPaper of rawPapers) { 86 | const curPaper = { 87 | id: eachPaper['id'], 88 | extractedID: eachPaper['id'].split('/').pop(), 89 | updated: eachPaper['updated'], 90 | published: eachPaper['published'], 91 | title: eachPaper['title'], 92 | summary: eachPaper['summary'], 93 | authors: eachPaper['author'], 94 | doi: eachPaper['arxiv:doi'] || '', 95 | journalRef: eachPaper['arxiv:journal_ref'] || '', 96 | primaryCategory: eachPaper['arxiv:primary_category'] || '', 97 | category: eachPaper['arxiv:category'] || '', 98 | comment: eachPaper['arxiv:comment'] || '', 99 | pdfLink: parsePDFLinkFromPaperID(eachPaper['id']) 100 | }; 101 | 102 | // Clean Title 103 | const cleanedTitle = removeNewLineCharacter(curPaper['title']); 104 | 105 | // Clean Summary 106 | const cleanedSummary = removeNewLineCharacter(curPaper['summary']); 107 | 108 | // Format Authors 109 | const authorList = []; 110 | try { 111 | for (const eachAuthor of curPaper['authors']) { 112 | authorList.push(eachAuthor['name']); 113 | } 114 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 115 | } catch (e: unknown) { 116 | authorList.push(eachPaper['author']['name']); 117 | } 118 | 119 | // Add to response 120 | curPaper['title'] = cleanedTitle; 121 | curPaper['summary'] = cleanedSummary; 122 | curPaper['authors'] = authorList; 123 | cleanedPapers.push(curPaper); 124 | } 125 | return cleanedPapers; 126 | } 127 | -------------------------------------------------------------------------------- /src/components/ai_chat/ai_chat.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import { Trash2, Minimize, Maximize } from 'lucide-svelte'; 3 | 4 | import Markdown from 'svelte-exmarkdown'; 5 | import { gfmPlugin } from 'svelte-exmarkdown/gfm'; 6 | const plugins = [gfmPlugin()]; 7 | import { aiConversationState } from '../../state/ai_conversation_state.svelte'; 8 | import SelectedPapers from './selected_papers.svelte'; 9 | import axios from 'axios'; 10 | 11 | let minimizeConversation = $state(false); 12 | let examplePrompts = [ 13 | 'Summarize this paper for me?', 14 | 'When was this paper published?', 15 | 'Explain this paper in simple terms?', 16 | 'What are the key points of this paper?', 17 | 'Compare and contrast these papers in table format?', 18 | 'Do you know anything about the authors?', 19 | 'Make a joke based on this paper', 20 | 'What is the pdf link?' 21 | ]; 22 | 23 | async function chatWithAI(promptContent: any) { 24 | aiConversationState.conversation.push({ 25 | from: 'user', 26 | content: promptContent 27 | }); 28 | aiConversationState.conversation.push({ 29 | from: 'system', 30 | content: 'thinking ...' 31 | }); 32 | const response = await axios.post('/api/ai_chat', { 33 | apiKey: '', 34 | selectedPapers: JSON.stringify(aiConversationState.selectedPapersList), 35 | conversation: JSON.stringify(aiConversationState.conversation), 36 | prompt: promptContent 37 | }); 38 | 39 | aiConversationState.conversation.pop(); 40 | aiConversationState.conversation.push({ 41 | from: 'ai', 42 | content: response.data 43 | }); 44 | } 45 | </script> 46 | 47 | <div 48 | class={minimizeConversation == true 49 | ? 'no-scrollbar max-h-8 min-h-8 overflow-clip p-3' 50 | : 'no-scrollbar max-h-[600px] min-h-10 overflow-clip p-3'} 51 | > 52 | <!-- AI Options --> 53 | <div class="flex w-full items-center justify-between pb-2"> 54 | <!-- Selected Papers --> 55 | <SelectedPapers /> 56 | 57 | <!-- Clear / Minimize Conversation --> 58 | <div class="flex cursor-pointer items-center gap-2 pr-2 text-zinc-500"> 59 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 60 | <Trash2 61 | size={14} 62 | class="hover:text-black" 63 | onclick={() => (aiConversationState.conversation = [])} 64 | /> 65 | </div> 66 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 67 | <Trash2 68 | size={16} 69 | class="hover:text-black" 70 | onclick={() => (aiConversationState.conversation = [])} 71 | /> 72 | </div> 73 | {#if minimizeConversation == true} 74 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 75 | <Maximize 76 | size={14} 77 | class="hover:text-black" 78 | onclick={() => (minimizeConversation = !minimizeConversation)} 79 | /> 80 | </div> 81 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 82 | <Maximize 83 | size={16} 84 | class="hover:text-black" 85 | onclick={() => (minimizeConversation = !minimizeConversation)} 86 | /> 87 | </div> 88 | {:else} 89 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 90 | <Minimize 91 | size={14} 92 | class="hover:text-black" 93 | onclick={() => (minimizeConversation = !minimizeConversation)} 94 | /> 95 | </div> 96 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 97 | <Minimize 98 | size={16} 99 | class="hover:text-black" 100 | onclick={() => (minimizeConversation = !minimizeConversation)} 101 | /> 102 | </div> 103 | {/if} 104 | </div> 105 | </div> 106 | 107 | <!-- Chats --> 108 | <div 109 | class={aiConversationState.conversation.length > 0 110 | ? 'no-scrollbar max-h-[580px] overflow-scroll pb-24 pt-4' 111 | : 'no-scrollbar pb-0'} 112 | > 113 | {#if aiConversationState.conversation.length > 0} 114 | {#each aiConversationState.conversation as eachMessage} 115 | <div 116 | class={eachMessage.from == 'user' 117 | ? 'no-scrollbar flex w-full justify-end py-1' 118 | : 'no-scrollbar flex w-full justify-start py-1'} 119 | > 120 | <div 121 | class={eachMessage.from == 'system' 122 | ? 'no-scrollbar max-w-[80%] animate-pulse rounded-xl border bg-white px-3 py-1 text-sm' 123 | : 'no-scrollbar max-w-[80%] rounded-xl border bg-white px-3 py-1 text-sm'} 124 | > 125 | <Markdown md={eachMessage['content']} {plugins} /> 126 | </div> 127 | </div> 128 | {/each} 129 | {:else} 130 | <div 131 | class={'no-scrollbar flex max-h-[580px] w-full flex-col items-center justify-center gap-y-2 overflow-scroll py-1 pb-24 pt-14'} 132 | > 133 | {#each examplePrompts as eachPrompt} 134 | <!-- svelte-ignore a11y_click_events_have_key_events --> 135 | <!-- svelte-ignore a11y_no_static_element_interactions --> 136 | <div 137 | class="no-scrollbar w-fit max-w-[80%] cursor-pointer rounded-xl border bg-white px-3 py-1 text-sm hover:border-zinc-400" 138 | onclick={() => chatWithAI(eachPrompt)} 139 | > 140 | <Markdown md={eachPrompt} {plugins} /> 141 | </div> 142 | {/each} 143 | </div> 144 | {/if} 145 | </div> 146 | </div> 147 | -------------------------------------------------------------------------------- /src/components/each_comment/each_comment.svelte: -------------------------------------------------------------------------------- 1 | <script> 2 | import { 3 | CalendarDays, 4 | Delete, 5 | DeleteIcon, 6 | Heart, 7 | MessageCircle, 8 | Trash2Icon, 9 | User 10 | } from 'lucide-svelte'; 11 | import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; 12 | import { createEventDispatcher } from 'svelte'; 13 | import * as Avatar from '$lib/components/ui/avatar/index'; 14 | 15 | const dispatch = createEventDispatcher(); 16 | 17 | import moment from 'moment'; 18 | import { authClient } from '$lib/auth_client'; 19 | let { comment } = $props(); 20 | 21 | // Readable Time 22 | const timestamp = comment['createdAt']; 23 | const now = moment(); 24 | const commentTime = moment(timestamp); 25 | let readableTime = $state(); 26 | 27 | if (now.isSame(commentTime, 'day')) { 28 | readableTime = commentTime.format('h:mm A'); // Show time only if today 29 | } else if (now.diff(commentTime, 'days') <= 30) { 30 | readableTime = now.diff(commentTime, 'days') + ' days ago'; // Show days ago if recent 31 | } else { 32 | readableTime = commentTime.format('MMM Do YYYY'); // Show month, day, year if older than a month 33 | } 34 | 35 | const session = authClient.useSession(); 36 | 37 | const isCurrentUserComment = $derived($session.data?.user.email === comment.commenter.email); 38 | let isDeleting = $state(false); 39 | 40 | async function deleteComment() { 41 | if (isDeleting) return; // Prevent multiple clicks 42 | 43 | isDeleting = true; 44 | try { 45 | const response = await fetch('/api/delete_comment', { 46 | method: 'DELETE', 47 | headers: { 48 | 'Content-Type': 'application/json' 49 | }, 50 | body: JSON.stringify({ 51 | commentId: comment._id, 52 | extractedID: comment.extractedID 53 | }) 54 | }); 55 | 56 | if (!response.ok) { 57 | const error = await response.json(); 58 | throw new Error(error.error || 'Failed to delete comment'); 59 | } 60 | 61 | // Dispatch an event to notify the parent component to update the comments list 62 | dispatch('commentDeleted', { commentId: comment._id }); 63 | } catch (error) { 64 | console.error('Error deleting comment:', error); 65 | // Dispatch an error event that parent can listen to 66 | // dispatch('deleteError', { message: error.message }); 67 | } finally { 68 | isDeleting = false; 69 | } 70 | } 71 | </script> 72 | 73 | <div class="inline-flex items-start gap-x-3"> 74 | <DropdownMenu.Root> 75 | <DropdownMenu.Trigger> 76 | <div class="flex cursor-pointer items-center gap-x-2"> 77 | <Avatar.Root class="border border-zinc-300 drop-shadow-md hover:shadow-lg"> 78 | <Avatar.Image src="" /> 79 | <Avatar.Fallback> 80 | {comment.commenter.name.includes(' ') 81 | ? comment.commenter.name 82 | .split(' ') 83 | .map((/** @type {any[]} */ n) => n[0]) 84 | .join('') 85 | .toUpperCase() 86 | : comment.commenter.name[0].toString().toUpperCase()} 87 | </Avatar.Fallback> 88 | </Avatar.Root> 89 | </div> 90 | </DropdownMenu.Trigger> 91 | <DropdownMenu.Content> 92 | <DropdownMenu.Group> 93 | <DropdownMenu.Item 94 | class="flex cursor-pointer justify-center gap-1.5 text-xs hover:text-red-500" 95 | > 96 | <span class="text-xs font-semibold"> 97 | {comment.commenter.name} 98 | </span> 99 | </DropdownMenu.Item> 100 | </DropdownMenu.Group> 101 | </DropdownMenu.Content> 102 | </DropdownMenu.Root> 103 | 104 | <div 105 | class="w-fit cursor-pointer items-center gap-x-3 rounded-xl border border-zinc-300 bg-white pb-1 pr-4 pt-2 text-zinc-800 drop-shadow-md transition-all duration-300 ease-in-out hover:drop-shadow-lg group-hover:border-black group-hover:text-black" 106 | > 107 | <!-- Commenter and Date --> 108 | <div class="flex justify-between gap-x-4 pl-4 text-xs"> 109 | <div class="flex items-center gap-x-1"> 110 | <User size={12} /> 111 | <span class="pb-[1px]"> 112 | {comment.commenter.name} 113 | </span> 114 | </div> 115 | <div class="flex items-center"> 116 | <CalendarDays size={12} /> 117 | <span class="pl-1"> 118 | {readableTime} 119 | </span> 120 | </div> 121 | 122 | {#if isCurrentUserComment} 123 | <DropdownMenu.Root> 124 | <DropdownMenu.Trigger> 125 | <div class="flex cursor-pointer items-center hover:text-red-500"> 126 | <Trash2Icon size={12} /> 127 | </div> 128 | </DropdownMenu.Trigger> 129 | <DropdownMenu.Content> 130 | <DropdownMenu.Group> 131 | <DropdownMenu.Item 132 | class="flex cursor-pointer items-center gap-1.5 text-xs hover:text-red-500" 133 | onclick={deleteComment} 134 | disabled={isDeleting} 135 | > 136 | {#if isDeleting} 137 | <svg 138 | class="-ml-1 mr-1 h-3 w-3 animate-spin text-red-500" 139 | xmlns="http://www.w3.org/2000/svg" 140 | fill="none" 141 | viewBox="0 0 24 24" 142 | > 143 | <circle 144 | class="opacity-25" 145 | cx="12" 146 | cy="12" 147 | r="10" 148 | stroke="currentColor" 149 | stroke-width="4" 150 | ></circle> 151 | <path 152 | class="opacity-75" 153 | fill="currentColor" 154 | d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 155 | ></path> 156 | </svg> 157 | Deleting... 158 | {:else} 159 | <Trash2Icon size={12} class="mr-0.5" /> 160 | Delete 161 | {/if} 162 | </DropdownMenu.Item> 163 | </DropdownMenu.Group> 164 | </DropdownMenu.Content> 165 | </DropdownMenu.Root> 166 | {/if} 167 | </div> 168 | 169 | <!-- Content --> 170 | <div class="max-w-full py-3 pl-4 text-sm"> 171 | <span class="break-words">{comment.comment}</span> 172 | <!-- <span> {eachCommentState.comment['comment']}</span> --> 173 | </div> 174 | </div> 175 | </div> 176 | -------------------------------------------------------------------------------- /src/components/main_input/advanced_search.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import * as Dialog from '$lib/components/ui/dialog/index.js'; 3 | import * as Select from '$lib/components/ui/select/index.js'; 4 | import axios from 'axios'; 5 | import { inputState } from '../../state/input_state.svelte'; 6 | import { paperListState } from '../../state/papers_list.svelte'; 7 | import LabelAndInputBox from './label_and_input_box.svelte'; 8 | import { Circle } from 'svelte-loading-spinners'; 9 | 10 | async function resetAdvancedSearch() { 11 | inputState.advancedSearch = false; 12 | inputState.id = ''; 13 | inputState.ti = ''; 14 | inputState.au = ''; 15 | inputState.abs = ''; 16 | inputState.co = ''; 17 | inputState.jr = ''; 18 | inputState.cat = ''; 19 | inputState.rn = ''; 20 | inputState.sortBy = 'Sort By'; 21 | inputState.sortOrder = 'Sort Order'; 22 | inputState.maxResults = 10; 23 | inputState.startIndex = 0; 24 | inputState.isSearching = true; 25 | const response = await axios.post('/api/search_papers', { 26 | searchFilterString: { 27 | all: 'electron' 28 | } 29 | }); 30 | paperListState.paperList = []; 31 | paperListState.paperList = []; 32 | paperListState.paperList = response.data; 33 | inputState.isSearching = false; 34 | inputState.lastSearch = 'electron'; 35 | } 36 | 37 | async function advancedSearchPaper() { 38 | inputState.isSearching = true; 39 | inputState.advancedSearch = true; 40 | paperListState.paperList = []; 41 | const response = await axios.post('/api/search_papers', { 42 | startIndex: 0, 43 | maxResults: inputState.maxResults || 10, 44 | searchFilterString: { 45 | id: inputState.id, 46 | ti: inputState.ti.replace(':', ''), 47 | au: inputState.au, 48 | abs: inputState.abs, 49 | co: inputState.co, 50 | jr: inputState.jr, 51 | cat: inputState.cat, 52 | rn: inputState.rn 53 | }, 54 | sortBy: inputState.sortBy == 'Sort By' ? 'relevance' : inputState.sortBy, 55 | sortOrder: inputState.sortOrder == 'Sort Order' ? 'ascending' : inputState.sortOrder 56 | }); 57 | // inputState.lastSearch = inputState.searchContent; 58 | paperListState.paperList = response.data; 59 | inputState.isSearching = false; 60 | } 61 | </script> 62 | 63 | <Dialog.Content> 64 | <Dialog.Header> 65 | <Dialog.Title>Advanced Search</Dialog.Title> 66 | <Dialog.Description>Fill out only the information you want below</Dialog.Description> 67 | </Dialog.Header> 68 | 69 | <div class="flex flex-col gap-y-2"> 70 | <LabelAndInputBox bind:state={inputState.id} label="ID" placeholder="ID ..." /> 71 | <LabelAndInputBox bind:state={inputState.ti} label="Title" placeholder="title ..." /> 72 | <LabelAndInputBox 73 | bind:state={inputState.au} 74 | label="Authors" 75 | placeholder="separated by comma ..." 76 | /> 77 | <LabelAndInputBox bind:state={inputState.abs} label="Abstract" placeholder="abstract ..." /> 78 | <LabelAndInputBox bind:state={inputState.co} label="Comment" placeholder="comment ..." /> 79 | <LabelAndInputBox bind:state={inputState.jr} label="Journal" placeholder="journal number ..." /> 80 | <LabelAndInputBox 81 | bind:state={inputState.cat} 82 | label="Category" 83 | placeholder="physics, maths ..." 84 | /> 85 | <LabelAndInputBox bind:state={inputState.rn} label="Rn" placeholder="rn ..." /> 86 | 87 | <div class="flex flex-col gap-y-2"> 88 | <div class="flex gap-x-3"> 89 | <Select.Root type="single" bind:value={inputState.sortBy}> 90 | <Select.Trigger 91 | class="w-[170px] rounded-lg border border-zinc-200 hover:border-zinc-400 focus:outline-none focus:ring-0" 92 | >{inputState.sortBy}</Select.Trigger 93 | > 94 | <Select.Content> 95 | <Select.Item value="relevance">Relevance</Select.Item> 96 | <Select.Item value="lastUpdatedDate">Last Updated Date</Select.Item> 97 | <Select.Item value="submittedDate">Submitted Date</Select.Item> 98 | </Select.Content> 99 | </Select.Root> 100 | <Select.Root type="single" bind:value={inputState.sortOrder}> 101 | <Select.Trigger 102 | class="w-[130px] rounded-lg border border-zinc-200 hover:border-zinc-400 focus:outline-none focus:ring-0" 103 | >{inputState.sortOrder}</Select.Trigger 104 | > 105 | <Select.Content> 106 | <Select.Item value="ascending">Ascending</Select.Item> 107 | <Select.Item value="descending">Descending</Select.Item> 108 | </Select.Content> 109 | </Select.Root> 110 | </div> 111 | <div class="flex items-center gap-x-2"> 112 | <div class="line-clamp-1 pl-2 text-left text-sm">Max Result</div> 113 | <div class="overflow-clip rounded-lg border border-zinc-200 bg-white hover:border-zinc-400"> 114 | <input 115 | type="number" 116 | min="1" 117 | max="100" 118 | defaultValue="1" 119 | bind:value={inputState.maxResults} 120 | class="w-fit bg-white px-3 py-1 text-sm outline-none" 121 | placeholder="number ..." 122 | /> 123 | </div> 124 | </div> 125 | </div> 126 | </div> 127 | 128 | <Dialog.Footer> 129 | <div class="flex w-full gap-x-2 pt-3"> 130 | {#if inputState.isSearching == true} 131 | <div> 132 | <div 133 | class="flex h-full w-36 items-center justify-center rounded-lg border border-zinc-400 bg-zinc-100 px-5 py-1" 134 | > 135 | <Circle size="22" color="#000000" duration="1s" /> 136 | </div> 137 | </div> 138 | {:else} 139 | <!-- svelte-ignore a11y_click_events_have_key_events --> 140 | <!-- svelte-ignore a11y_no_static_element_interactions --> 141 | <div 142 | class="w-fit cursor-pointer rounded-lg border border-zinc-400 bg-zinc-100 px-6 py-1 hover:border-zinc-500 hover:bg-black hover:text-white" 143 | onclick={() => advancedSearchPaper()} 144 | > 145 | Search Paper 146 | </div> 147 | {/if} 148 | 149 | <!-- svelte-ignore a11y_click_events_have_key_events --> 150 | <!-- svelte-ignore a11y_no_static_element_interactions --> 151 | <div 152 | class="w-fit cursor-pointer rounded-lg border border-zinc-300 bg-zinc-50 px-3 py-1 hover:border-red-500 hover:text-red-500" 153 | onclick={() => resetAdvancedSearch()} 154 | > 155 | Reset 156 | </div> 157 | </div> 158 | </Dialog.Footer> 159 | </Dialog.Content> 160 | -------------------------------------------------------------------------------- /src/routes/auth/sign_in/+page.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Button from '$lib/components/ui/button/button.svelte'; 3 | import * as Card from '$lib/components/ui/card'; 4 | import { Circle } from 'svelte-loading-spinners'; 5 | import BetterAuthRemark from '../../../components/remarks/better_auth_remark.svelte'; 6 | import Title from '../../../components/title.svelte'; 7 | import { authClient } from '$lib/auth_client'; 8 | 9 | import logo from '$lib/assets/logo/logo.png'; 10 | import { Drama, Github, Twitter } from 'lucide-svelte'; 11 | 12 | import huggingface from '$lib/assets/icons/huggingface.png'; 13 | import twitter from '$lib/assets/icons/twitter.png'; 14 | import google from '$lib/assets/icons/google.png'; 15 | import youtube from '$lib/assets/icons/youtube.png'; 16 | 17 | let isLogingInWithGithub = $state(false); 18 | let isLogingInWithGoogle = $state(false); 19 | let isLogingInWithHuggingFace = $state(false); 20 | let isLogingInWithTwitter = $state(false); 21 | let isLogingInWithAnonymous = $state(false); 22 | </script> 23 | 24 | <svelte:head> 25 | <title>ScholarXIV 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 |  51 |
52 | 53 | 54 | 55 | 56 | <div class="mt-5 space-y-2"> 57 | <div class="flex space-x-2"> 58 | <!-- Google Login --> 59 | <Button 60 | type="button" 61 | class="group/google w-full" 62 | onclick={async () => { 63 | isLogingInWithGoogle = !isLogingInWithGoogle; 64 | await authClient.signIn.social({ 65 | provider: 'google', 66 | callbackURL: '/homepage' 67 | }); 68 | }} 69 | > 70 | {#if isLogingInWithGoogle === true} 71 | <Circle size="22" color="#ffffff" duration="1s" /> 72 | {:else} 73 | <div class="flex items-center gap-x-2"> 74 | <img src={google} alt="" class="h-4 w-4" /> 75 | 76 | <span class="font-semibold group-hover/google:text-blue-400"> Google </span> 77 | </div> 78 | {/if} 79 | </Button> 80 | 81 | <!-- GitHub Login --> 82 | <Button 83 | type="button" 84 | class="group/github w-full" 85 | onclick={async () => { 86 | isLogingInWithGithub = !isLogingInWithGithub; 87 | await authClient.signIn.social({ 88 | provider: 'github', 89 | callbackURL: '/homepage' 90 | }); 91 | }} 92 | > 93 | {#if isLogingInWithGithub === true} 94 | <Circle size="22" color="#ffffff" duration="1s" /> 95 | {:else} 96 | <div class="flex items-center gap-x-2"> 97 | <Github size="22" /> 98 | <span class="font-semibold"> GitHub </span> 99 | </div> 100 | {/if} 101 | </Button> 102 | 103 | <!-- Twitter Login --> 104 | <Button 105 | type="button" 106 | class="group/twitter w-full" 107 | onclick={async () => { 108 | isLogingInWithTwitter = !isLogingInWithTwitter; 109 | await authClient.signIn.social({ 110 | provider: 'twitter', 111 | callbackURL: '/homepage' 112 | }); 113 | }} 114 | > 115 | {#if isLogingInWithTwitter === true} 116 | <Circle size="22" color="#ffffff" duration="1s" /> 117 | {:else} 118 | <div class="flex items-center gap-x-2"> 119 | <img src={twitter} alt="" class="h-4 w-4" /> 120 | <span class="font-semibold group-hover/twitter:text-cyan-300"> X (Twitter) </span> 121 | </div> 122 | {/if} 123 | </Button> 124 | </div> 125 | 126 | <div class="flex space-x-2"> 127 | <!-- HuggingFace Login --> 128 | <Button 129 | type="button" 130 | class="group/huggingface w-full" 131 | onclick={async () => { 132 | isLogingInWithHuggingFace = !isLogingInWithHuggingFace; 133 | await authClient.signIn.social({ 134 | provider: 'huggingface', 135 | callbackURL: '/homepage' 136 | }); 137 | }} 138 | > 139 | {#if isLogingInWithHuggingFace === true} 140 | <Circle size="22" color="#ffffff" duration="1s" /> 141 | {:else} 142 | <div class="flex items-center gap-x-2"> 143 | <img src={huggingface} alt="" class="h-4 w-4" /> 144 | <span class="font-semibold group-hover/huggingface:text-yellow-300"> 145 | Hugging Face 146 | </span> 147 | </div> 148 | {/if} 149 | </Button> 150 | 151 | <!-- Anonymous Login --> 152 | <Button 153 | type="button" 154 | class="group/anonymous w-full" 155 | onclick={async () => { 156 | isLogingInWithAnonymous = !isLogingInWithAnonymous; 157 | await authClient.signIn.anonymous(); 158 | }} 159 | > 160 | {#if isLogingInWithAnonymous === true} 161 | <Circle size="22" color="#ffffff" duration="1s" /> 162 | {:else} 163 | <div class="flex items-center gap-x-2"> 164 | <Drama size={17} /> 165 | <!-- <img src={huggingface} alt="" class="h-4 w-4" /> --> 166 | <span class="font-semibold"> Guest Mode </span> 167 | </div> 168 | {/if} 169 | </Button> 170 | </div> 171 | </div> 172 | </Card.Root> 173 | 174 | <!-- Better Auth Remark --> 175 | <BetterAuthRemark /> 176 | 177 | <!-- Demo --> 178 | <div class="flex items-center gap-x-2 pb-2 pt-10"> 179 | <img src={youtube} alt="" class="h-4" /> 180 | <a 181 | href="https://youtu.be/-GlxZRCfxYA" 182 | target="_blank" 183 | class="font-sm underline-offset-4 hover:text-blue-500 hover:underline">Demo Video</a 184 | > 185 | </div> 186 | 187 | <a 188 | href="https://www.producthunt.com/products/scholarxiv-2?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-scholarxiv-2" 189 | target="_blank" 190 | ><img 191 | src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=985666&theme=light&t=1751248793638" 192 | alt="ScholarXIV - Open-source and AI powered Research | Product Hunt" 193 | style="width: 200px; height: 40px;" 194 | width="250" 195 | height="54" 196 | /></a 197 | > 198 | </div> 199 | -------------------------------------------------------------------------------- /src/components/main_input/main_input.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | let { isCommentMode = false } = $props(); 3 | import { MessageCircle, Search, Settings2, Sparkles } from 'lucide-svelte'; 4 | import { inputState } from '../../state/input_state.svelte'; 5 | import axios from 'axios'; 6 | import { paperListState } from '../../state/papers_list.svelte'; 7 | import { Circle } from 'svelte-loading-spinners'; 8 | import AiChat from '../ai_chat/ai_chat.svelte'; 9 | import { aiConversationState } from '../../state/ai_conversation_state.svelte'; 10 | import InputSettings from './input_settings.svelte'; 11 | import SelectedPapers from '../ai_chat/selected_papers.svelte'; 12 | import { authClient } from '$lib/auth_client'; 13 | import { page } from '$app/state'; 14 | import { commentState } from '../../state/comment_state.svelte'; 15 | 16 | async function searchPaper() { 17 | if (inputState.searchContent.trim().length > 0) { 18 | inputState.isSearching = true; 19 | inputState.statusText = `Searching for `; 20 | inputState.lastSearch = inputState.searchContent; 21 | inputState.advancedSearch = false; 22 | paperListState.paperList = []; 23 | const response = await axios.post('/api/search_papers', { 24 | startIndex: inputState.startIndex, 25 | maxResults: inputState.maxResults, 26 | searchFilterString: { 27 | all: inputState.searchContent.replace(':', '') 28 | }, 29 | sortBy: inputState.sortBy == 'Sort By' ? 'relevance' : inputState.sortBy, 30 | sortOrder: inputState.sortOrder == 'Sort Order' ? 'ascending' : inputState.sortOrder 31 | }); 32 | paperListState.paperList = []; 33 | paperListState.paperList = response.data; 34 | inputState.isSearching = false; 35 | inputState.statusText = `Results for `; 36 | } 37 | } 38 | 39 | async function chatWithAI() { 40 | if (inputState.aiInput.trim().length > 0) { 41 | aiConversationState.conversation.push({ 42 | from: 'user', 43 | content: inputState.aiInput 44 | }); 45 | inputState.aiInput = ''; 46 | aiConversationState.conversation.push({ 47 | from: 'system', 48 | content: 'thinking ...' 49 | }); 50 | const response = await axios.post('/api/ai_chat', { 51 | selectedPapers: JSON.stringify(aiConversationState.selectedPapersList), 52 | conversation: JSON.stringify(aiConversationState.conversation), 53 | prompt: 54 | aiConversationState.conversation[aiConversationState.conversation.length - 2].content 55 | }); 56 | aiConversationState.conversation.pop(); 57 | aiConversationState.conversation.push({ 58 | from: 'ai', 59 | content: response.data 60 | }); 61 | } 62 | } 63 | 64 | async function commentOnPaper() { 65 | if (commentState.comment.trim().length > 0) { 66 | commentState.isCommenting = true; 67 | const extractedID = page.url.pathname.split('/').pop(); 68 | const response = await fetch('/api/comment_on_paper', { 69 | method: 'POST', 70 | headers: { 71 | 'Content-Type': 'application/json' 72 | }, 73 | body: JSON.stringify({ 74 | parentID: null, 75 | extractedID: extractedID, 76 | comment: commentState.comment 77 | }) 78 | }); 79 | const data = await response.json(); 80 | console.log(data); 81 | commentState.comment = ''; 82 | commentState.comments = data; 83 | commentState.isCommenting = false; 84 | } 85 | } 86 | 87 | function handleEnter(event: KeyboardEvent) { 88 | if (event.key === 'Enter') { 89 | event.preventDefault(); 90 | if (isAIMode == true) { 91 | chatWithAI(); 92 | } else if (isCommentMode == true) { 93 | commentOnPaper(); 94 | } else { 95 | searchPaper(); 96 | } 97 | } 98 | } 99 | 100 | let isAIMode = $state(false); 101 | 102 | // const isCommentMode = $state(page.url.pathname.split('/')[1] == 'comments'); 103 | let session = authClient.useSession(); 104 | </script> 105 | 106 | <div 107 | class="no-scrollbar absolute bottom-0 left-0 right-0 m-auto h-fit w-full rounded-tl-xl rounded-tr-xl border-t border-zinc-200 pb-4 108 | backdrop-blur-lg md:w-2/3 lg:w-2/4 xl:w-2/5 2xl:w-2/5" 109 | > 110 | {#if isAIMode == true} 111 | <AiChat /> 112 | {/if} 113 | 114 | <!-- Main Input Box --> 115 | <div class="no-scrollbar flex flex-col bg-transparent px-2 pt-2"> 116 | <!-- svelte-ignore a11y_click_events_have_key_events --> 117 | <div class="group flex overflow-clip rounded-3xl border border-zinc-400 bg-white"> 118 | <!-- Input Box --> 119 | <div class="flex w-full items-center gap-x-2 px-3 py-2"> 120 | {#if isAIMode == true} 121 | <Sparkles size={18} class="text-zinc-400" /> 122 | <input 123 | type="text" 124 | class="w-full items-center bg-white pb-0 outline-none md:pb-1 lg:pb-1 xl:pb-1 2xl:pb-1" 125 | placeholder={`Chat with ${aiConversationState.currentModel.name} ...`} 126 | bind:value={inputState.aiInput} 127 | onkeydown={handleEnter} 128 | /> 129 | {:else if isCommentMode == true} 130 | <MessageCircle size={18} class="text-zinc-400" /> 131 | <input 132 | type="text" 133 | class="w-full items-center bg-white pb-0 outline-none md:pb-1 lg:pb-1 xl:pb-1 2xl:pb-1" 134 | placeholder="Comment ..." 135 | bind:value={commentState.comment} 136 | onkeydown={handleEnter} 137 | /> 138 | {:else} 139 | <Search size={18} class="text-zinc-400" /> 140 | <input 141 | type="text" 142 | class="w-full items-center bg-white pb-0 outline-none md:pb-1 lg:pb-1 xl:pb-1 2xl:pb-1" 143 | placeholder="Search ..." 144 | bind:value={inputState.searchContent} 145 | onkeydown={handleEnter} 146 | /> 147 | {/if} 148 | </div> 149 | 150 | <!-- Settings and AI Toggle --> 151 | <!-- svelte-ignore a11y_no_static_element_interactions --> 152 | <div class="m-auto flex items-center pr-1"> 153 | <InputSettings {isAIMode} {isCommentMode} /> 154 | {#if isAIMode} 155 | <div 156 | class="cursor-pointer rounded-full p-2 text-zinc-600 hover:bg-zinc-100 hover:text-black" 157 | onclick={() => (isAIMode = !isAIMode)} 158 | > 159 | {#if isCommentMode == false} 160 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 161 | <Search size={14} /> 162 | </div> 163 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 164 | <Search size={17} /> 165 | </div> 166 | {:else} 167 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 168 | <MessageCircle size={14} /> 169 | </div> 170 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 171 | <MessageCircle size={17} /> 172 | </div> 173 | {/if} 174 | </div> 175 | {:else if $session.data} 176 | <div 177 | class="cursor-pointer rounded-full p-2 text-zinc-600 hover:bg-zinc-100 hover:text-black" 178 | onclick={() => (isAIMode = !isAIMode)} 179 | > 180 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 181 | <Sparkles size={14} /> 182 | </div> 183 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 184 | <Sparkles size={17} /> 185 | </div> 186 | </div> 187 | {/if} 188 | </div> 189 | 190 | <!-- Search Button --> 191 | {#if inputState.isSearching == true || commentState.isCommenting == true} 192 | <div> 193 | <div class="flex h-full w-20 items-center justify-center border-l"> 194 | <Circle size="22" color="#000000" duration="1s" /> 195 | </div> 196 | </div> 197 | {:else} 198 | <!-- svelte-ignore a11y_no_static_element_interactions --> 199 | <div 200 | class="group/search group-hover:bg-zinc-200" 201 | onclick={async () => { 202 | if (isAIMode == true) { 203 | await chatWithAI(); 204 | } else if (isCommentMode == true) { 205 | await commentOnPaper(); 206 | } else { 207 | await searchPaper(); 208 | } 209 | }} 210 | > 211 | <div 212 | class="flex h-full w-24 cursor-pointer items-center justify-center border-l group-hover/search:bg-black group-hover/search:text-white" 213 | > 214 | <span> 215 | {isAIMode == true ? 'Send' : isCommentMode == true ? 'Comment' : 'Search'} 216 | </span> 217 | </div> 218 | </div> 219 | {/if} 220 | </div> 221 | </div> 222 | </div> 223 | -------------------------------------------------------------------------------- /src/components/each_paper/interactions.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import axios from 'axios'; 3 | import moment from 'moment'; 4 | import { toast } from 'svelte-sonner'; 5 | import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; 6 | import { Bookmark, Download, Heart, Link2, MessageCircle, Scroll } from 'lucide-svelte'; 7 | import { authClient } from '$lib/auth_client'; 8 | 9 | // Props 10 | let { paperState } = $props(); 11 | 12 | // Readable Time 13 | const timestamp = paperState.paper['published']; 14 | const readableTime = moment(timestamp).format('MMM Do YYYY'); 15 | 16 | // Function to handle download 17 | async function handleDownload(paper: any) { 18 | // Show Toast 19 | toast.loading(`Downloading paper...`); 20 | 21 | // Download 22 | const response = await axios({ 23 | url: paper['pdfLink'], 24 | method: 'GET', 25 | responseType: 'blob' 26 | }); 27 | 28 | const url = window.URL.createObjectURL(new Blob([response.data])); 29 | const link = document.createElement('a'); 30 | link.href = url; 31 | link.setAttribute('download', paper['id'].split('/').pop() + '.pdf'); 32 | document.body.appendChild(link); 33 | link.click(); 34 | document.body.removeChild(link); 35 | 36 | // Show Toast 37 | toast.success(`Downloaded "${paper.title}" successfully! `); 38 | } 39 | 40 | // Function to copy to clipboard 41 | function copyToClipboard(type: string, paper: any) { 42 | let whatToCopy: string; 43 | 44 | // All: () => `ID: ${paper['id']}\nPublished Date: ${readableTime}\n\nTitle: ${paper['title']}\n\nAuthors: ${paper['authors'].join(', ')}\n\nSummary: ${paper['summary']}\n\nPDF Link: ${paper['pdfLink']}`, 45 | 46 | const copyMap: any = { 47 | All: () => `Paper: ${paper['id']} — ${paper['title']}...`, 48 | ID: () => paper['id'], 49 | Title: () => paper['title'], 50 | Authors: () => paper['authors'].join(', '), 51 | 'PDF Link': () => paper['pdfLink'], 52 | Summary: () => { 53 | const sentences = paper['summary'].split('. '); 54 | return sentences.slice(0, 1).join('. ') + (sentences.length > 2 ? '...' : ''); 55 | }, 56 | 'Published Date': () => readableTime 57 | }; 58 | 59 | whatToCopy = copyMap[type] ? copyMap[type]() : ''; 60 | 61 | // Copy to Clipboard 62 | navigator.clipboard.writeText(type === 'Summary' ? paper['summary'] : whatToCopy); 63 | 64 | // Show Toast 65 | toast.success(`Copied ${type} Successfully!`, { 66 | description: whatToCopy 67 | }); 68 | } 69 | 70 | // async function bookmarkPaper() {} 71 | const session = authClient.useSession(); 72 | </script> 73 | 74 | <!-- Interactions --> 75 | <!-- svelte-ignore a11y_no_static_element_interactions --> 76 | <div 77 | class="flex justify-between gap-x-2 pt-1 78 | text-xs md:justify-start lg:justify-start xl:justify-start 2xl:justify-start 79 | " 80 | > 81 | <!-- && $session.data.user.isAnonymous !== true to disable for anonymous users --> 82 | {#if $session.data} 83 | <!-- LIKE --> 84 | <!-- svelte-ignore a11y_click_events_have_key_events --> 85 | <div 86 | class="flex w-fit items-center gap-x-2 rounded-2xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-100 hover:text-black" 87 | onclick={(e) => { 88 | paperState.toggleLike($session.data?.user.id, paperState.paper.extractedID); 89 | e.stopPropagation(); 90 | }} 91 | > 92 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 93 | <Heart 94 | size={15} 95 | fill={paperState.isLiked == true ? 'red' : 'white'} 96 | class={paperState.isLiked == true ? 'text-red-500' : ''} 97 | /> 98 | </div> 99 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 100 | <Heart 101 | size={18} 102 | fill={paperState.isLiked == true ? 'red' : 'white'} 103 | class={paperState.isLiked == true ? 'text-red-500' : ''} 104 | /> 105 | </div> 106 | 107 | <span class="flex pb-[2px]"> 108 | {paperState.likeCount} 109 | </span> 110 | </div> 111 | 112 | <!-- COMMENT --> 113 | <a href="/comments/papers/{paperState.paper['extractedID']}"> 114 | <div 115 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-100 hover:text-black" 116 | > 117 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 118 | <MessageCircle size={15} /> 119 | </div> 120 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 121 | <MessageCircle size={18} /> 122 | </div> 123 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Comments </span> 124 | </div> 125 | </a> 126 | {/if} 127 | <!-- SUMMARY --> 128 | <!-- svelte-ignore a11y_click_events_have_key_events --> 129 | <div 130 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-100 hover:text-black" 131 | onclick={(e) => { 132 | paperState.toggleSummary(); 133 | e.stopPropagation(); 134 | }} 135 | > 136 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 137 | <Scroll size={14} /> 138 | </div> 139 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 140 | <Scroll size={17} /> 141 | </div> 142 | 143 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Summary </span> 144 | </div> 145 | 146 | <!-- DOWNLOAD --> 147 | <!-- svelte-ignore a11y_click_events_have_key_events --> 148 | <div 149 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-100 hover:text-black" 150 | onclick={() => handleDownload(paperState.paper)} 151 | > 152 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 153 | <Download size={15} /> 154 | </div> 155 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 156 | <Download size={18} /> 157 | </div> 158 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Download </span> 159 | </div> 160 | 161 | {#if $session.data} 162 | <!-- BOOKMARK --> 163 | <!-- svelte-ignore a11y_click_events_have_key_events --> 164 | <!-- svelte-ignore a11y_click_events_have_key_events --> 165 | <div 166 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-100 hover:text-black" 167 | onclick={(e) => { 168 | paperState.toggleBookmark($session.data?.user.id, paperState.paper.extractedID); 169 | e.stopPropagation(); 170 | }} 171 | > 172 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 173 | <Bookmark 174 | size={15} 175 | fill={paperState.isBookmarked == true ? 'lightGreen' : 'white'} 176 | class={paperState.isBookmarked == true ? 'text-emerald-500' : ''} 177 | /> 178 | </div> 179 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 180 | <Bookmark 181 | size={18} 182 | fill={paperState.isBookmarked == true ? 'lightGreen' : 'white'} 183 | class={paperState.isBookmarked == true ? 'text-emerald-500' : ''} 184 | /> 185 | </div> 186 | 187 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> 188 | {paperState.isBookmarked ? 'Unbookmark' : 'Bookmark'} 189 | </span> 190 | </div> 191 | {/if} 192 | 193 | <!-- COPY --> 194 | <div> 195 | <DropdownMenu.Root> 196 | <DropdownMenu.Trigger> 197 | <div 198 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-100 hover:text-black" 199 | > 200 | <div class="hidden md:flex lg:flex xl:flex 2xl:flex"> 201 | <Link2 size={15} /> 202 | </div> 203 | <div class="flex md:hidden lg:hidden xl:hidden 2xl:hidden"> 204 | <Link2 size={18} /> 205 | </div> 206 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Copy </span> 207 | </div> 208 | </DropdownMenu.Trigger> 209 | <DropdownMenu.Content> 210 | <DropdownMenu.Group> 211 | <DropdownMenu.Item 212 | class="cursor-pointer text-xs" 213 | onclick={(event) => { 214 | event.stopPropagation(); 215 | copyToClipboard('All', paperState.paper); 216 | console.log('allhere'); 217 | }}>All</DropdownMenu.Item 218 | > 219 | <DropdownMenu.Item 220 | class="cursor-pointer text-xs" 221 | onclick={(event) => { 222 | event.stopPropagation(); 223 | copyToClipboard('ID', paperState.paper); 224 | }}>ID</DropdownMenu.Item 225 | > 226 | <DropdownMenu.Item 227 | class="cursor-pointer text-xs" 228 | onclick={(event) => { 229 | event.stopPropagation(); 230 | copyToClipboard('Title', paperState.paper); 231 | }}>Title</DropdownMenu.Item 232 | > 233 | <DropdownMenu.Item 234 | class="cursor-pointer text-xs" 235 | onclick={(event) => { 236 | event.stopPropagation(); 237 | copyToClipboard('Authors', paperState.paper); 238 | }}>Authors</DropdownMenu.Item 239 | > 240 | <DropdownMenu.Item 241 | class="cursor-pointer text-xs" 242 | onclick={(event) => { 243 | event.stopPropagation(); 244 | copyToClipboard('PDF Link', paperState.paper); 245 | }}>PDF Link</DropdownMenu.Item 246 | > 247 | <DropdownMenu.Item 248 | class="cursor-pointer text-xs" 249 | onclick={(event) => { 250 | event.stopPropagation(); 251 | copyToClipboard('Summary', paperState.paper); 252 | }}>Summary</DropdownMenu.Item 253 | > 254 | <DropdownMenu.Item 255 | class="cursor-pointer text-xs" 256 | onclick={(event) => { 257 | event.stopPropagation(); 258 | copyToClipboard('Published Date', paperState.paper); 259 | }}>Published Date</DropdownMenu.Item 260 | > 261 | </DropdownMenu.Group> 262 | </DropdownMenu.Content> 263 | </DropdownMenu.Root> 264 | </div> 265 | </div> 266 | -------------------------------------------------------------------------------- /src/components/each_paper_old.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import axios from 'axios'; 3 | import moment from 'moment'; 4 | import { 5 | Frame, 6 | CalendarDays, 7 | ExternalLink, 8 | User, 9 | Bookmark, 10 | BookmarkCheck, 11 | Download, 12 | Link2, 13 | Sparkles, 14 | Scroll, 15 | Heart, 16 | MessageCircle 17 | } from 'lucide-svelte'; 18 | import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; 19 | import { toast } from 'svelte-sonner'; 20 | import EachPaperState from '../state/each_paper_state.svelte'; 21 | 22 | // Paper 23 | let { paper }: { paper: any } = $props(); 24 | let eachPaperState = new EachPaperState(paper); 25 | 26 | // Readable Time 27 | const timestamp = paper['published']; 28 | const readableTime = moment(timestamp).format('MMM Do YYYY'); 29 | 30 | // Function to handle download 31 | async function handleDownload(paper: any) { 32 | console.log('downloading...'); 33 | const response = await axios({ 34 | url: paper['pdfLink'], 35 | method: 'GET', 36 | responseType: 'blob' 37 | }); 38 | 39 | console.log(response.data); 40 | const url = window.URL.createObjectURL(new Blob([response.data])); 41 | const link = document.createElement('a'); 42 | link.href = url; 43 | link.setAttribute('download', paper['id'].split('/').pop() + '.pdf'); 44 | document.body.appendChild(link); 45 | link.click(); 46 | document.body.removeChild(link); 47 | } 48 | 49 | // Function to copy to clipboard 50 | function copyToClipboard(type: string, paper: any) { 51 | let whatToCopy: string; 52 | 53 | const copyMap: any = { 54 | All: () => 55 | `ID: ${paper['id']}\nPublished Date: ${readableTime}\n\nTitle: ${paper['title']}\n\nAuthors: ${paper['authors'].join(', ')}\n\nSummary: ${paper['summary']}\n\nPDF Link: ${paper['pdfLink']}`, 56 | ID: () => paper['id'], 57 | Title: () => paper['title'], 58 | Authors: () => paper['authors'].join(', '), 59 | 'PDF Link': () => paper['pdfLink'], 60 | Summary: () => { 61 | const sentences = paper['summary'].split('. '); 62 | return sentences.slice(0, 1).join('. ') + (sentences.length > 2 ? '...' : ''); 63 | }, 64 | 'Published Date': () => readableTime 65 | }; 66 | 67 | whatToCopy = copyMap[type] ? copyMap[type]() : ''; 68 | 69 | // Copy to Clipboard 70 | navigator.clipboard.writeText(type === 'Summary' ? paper['summary'] : whatToCopy); 71 | 72 | // Show Toast 73 | toast.success(`Copied ${type} Successfully!`, { 74 | description: whatToCopy 75 | }); 76 | } 77 | 78 | // 79 | let isReadingSummary = $state(false); 80 | </script> 81 | 82 | <!-- svelte-ignore a11y_click_events_have_key_events --> 83 | <!-- svelte-ignore a11y_no_static_element_interactions --> 84 | <div class="no-scrollbar group"> 85 | <div 86 | class={'no-scrollbar relative flex cursor-pointer flex-col overflow-scroll rounded-xl border border-zinc-300 bg-white py-3 text-zinc-600 drop-shadow-xl transition-all duration-300 ease-in-out group-hover:border-black group-hover:text-black'} 87 | > 88 | <div class="px-4"> 89 | <!-- Date and ID --> 90 | <div class="flex items-center justify-between pb-1"> 91 | <div class="flex gap-x-4"> 92 | <div class="flex items-center text-xs"> 93 | <CalendarDays size={12} /> 94 | <span class="pl-1"> 95 | {readableTime} 96 | </span> 97 | </div> 98 | 99 | <div class="flex items-center text-xs"> 100 | <Frame size={12} /> 101 | <span class="pb-[1px] pl-1"> 102 | {paper['extractedID']} 103 | </span> 104 | </div> 105 | 106 | <!-- {#each selectedPapersState.selectedPapersID as id} 107 | {id} 108 | {/each} --> 109 | </div> 110 | <!-- <div class={$isBookmarked ? 'w-2 h-2 rounded-full bg-emerald-300' : ''}></div> --> 111 | </div> 112 | 113 | <!-- Title --> 114 | <a 115 | href={paper['pdfLink']} 116 | target="_blank" 117 | rel="noopener noreferrer" 118 | download 119 | class={'group/title font-semibold text-zinc-500 transition-all duration-300 ease-in-out group-hover:text-black '} 120 | > 121 | <span class="decoration-zinc-400 decoration-1 underline-offset-4 hover:underline"> 122 | {paper['title']} 123 | </span> 124 | <div class="hidden group-hover/title:inline-block"> 125 | <ExternalLink size={12} /> 126 | </div> 127 | </a> 128 | 129 | <!-- Authors --> 130 | <div class="no-scrollbar flex items-center gap-x-1 overflow-scroll pb-2 pt-1"> 131 | <User size={12} /> 132 | {#each paper['authors'] as eachAuthor} 133 | <div class="no-scrollbar linc w-fit pb-[1px] text-xs italic"> 134 | <span> 135 | {eachAuthor}, 136 | </span> 137 | </div> 138 | {/each} 139 | </div> 140 | 141 | <!-- Interactions --> 142 | <!-- svelte-ignore a11y_no_static_element_interactions --> 143 | <div 144 | class="flex justify-between gap-x-2 pt-1 145 | text-xs md:justify-start lg:justify-start xl:justify-start 2xl:justify-start 146 | " 147 | > 148 | <!-- svelte-ignore a11y_click_events_have_key_events --> 149 | <div 150 | class="flex w-fit items-center gap-x-2 rounded-2xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-200 hover:text-black" 151 | onclick={() => eachPaperState.likePaper()} 152 | > 153 | <Heart 154 | size={15} 155 | fill={eachPaperState.isLiked == true ? 'red' : 'white'} 156 | class={eachPaperState.isLiked == true ? 'text-red-500' : ''} 157 | /> 158 | <span class="flex pb-[2px]"> 159 | {eachPaperState.likes} 160 | </span> 161 | </div> 162 | 163 | <a href="/comments/paper/{paper['extractedID']}"> 164 | <div 165 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-200 hover:text-black" 166 | > 167 | <MessageCircle size={15} /> 168 | <span class="flex pl-1"> 169 | {eachPaperState.commentCount} 170 | </span> 171 | </div> 172 | </a> 173 | 174 | <div 175 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-200 hover:text-black" 176 | onclick={() => (isReadingSummary = !isReadingSummary)} 177 | > 178 | <Scroll size={14} /> 179 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Summary </span> 180 | </div> 181 | 182 | <div 183 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-200 hover:text-black" 184 | > 185 | <Download size={15} /> 186 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Download </span> 187 | </div> 188 | 189 | <!-- svelte-ignore a11y_click_events_have_key_events --> 190 | <div 191 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-200 hover:text-black" 192 | onclick={() => eachPaperState.bookmarkPaper()} 193 | > 194 | <Bookmark 195 | size={15} 196 | fill={eachPaperState.isBookmarked == true ? 'lightGreen' : 'white'} 197 | class={eachPaperState.isBookmarked == true ? 'text-emerald-500' : ''} 198 | /> 199 | 200 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> 201 | {eachPaperState.isBookmarked ? 'Unbookmark' : 'Bookmark'} 202 | </span> 203 | </div> 204 | 205 | <div> 206 | <DropdownMenu.Root> 207 | <DropdownMenu.Trigger> 208 | <div 209 | class="flex w-fit items-center gap-x-1 rounded-xl border border-transparent px-2 py-1 transition-all duration-200 ease-in-out hover:bg-zinc-200 hover:text-black" 210 | > 211 | <Link2 size={15} /> 212 | <span class="hidden md:flex lg:flex xl:flex 2xl:flex"> Copy </span> 213 | </div> 214 | </DropdownMenu.Trigger> 215 | <DropdownMenu.Content> 216 | <DropdownMenu.Group> 217 | <DropdownMenu.Item 218 | class="text-xs" 219 | on:click={(event) => { 220 | event.stopPropagation(); 221 | copyToClipboard('All', paper); 222 | }}>All</DropdownMenu.Item 223 | > 224 | <DropdownMenu.Item 225 | class="text-xs" 226 | on:click={(event) => { 227 | event.stopPropagation(); 228 | copyToClipboard('ID', paper); 229 | }}>ID</DropdownMenu.Item 230 | > 231 | <DropdownMenu.Item 232 | class="text-xs" 233 | on:click={(event) => { 234 | event.stopPropagation(); 235 | copyToClipboard('Title', paper); 236 | }}>Title</DropdownMenu.Item 237 | > 238 | <DropdownMenu.Item 239 | class="text-xs" 240 | on:click={(event) => { 241 | event.stopPropagation(); 242 | copyToClipboard('Authors', paper); 243 | }}>Authors</DropdownMenu.Item 244 | > 245 | <DropdownMenu.Item 246 | class="text-xs" 247 | on:click={(event) => { 248 | event.stopPropagation(); 249 | copyToClipboard('PDF Link', paper); 250 | }}>PDF Link</DropdownMenu.Item 251 | > 252 | <DropdownMenu.Item 253 | class="text-xs" 254 | on:click={(event) => { 255 | event.stopPropagation(); 256 | copyToClipboard('Summary', paper); 257 | }}>Summary</DropdownMenu.Item 258 | > 259 | <DropdownMenu.Item 260 | class="text-xs" 261 | on:click={(event) => { 262 | event.stopPropagation(); 263 | copyToClipboard('Published Date', paper); 264 | }}>Published Date</DropdownMenu.Item 265 | > 266 | </DropdownMenu.Group> 267 | </DropdownMenu.Content> 268 | </DropdownMenu.Root> 269 | </div> 270 | </div> 271 | </div> 272 | 273 | <!-- Summary --> 274 | <div class="px-3"> 275 | <div class={'hidden pt-3 text-sm transition-all duration-300 ease-in-out group-hover:flex'}> 276 | <div class="pt-3 text-sm transition-all duration-300 ease-in-out"> 277 | <div 278 | class="rounded-lg rounded-bl-md rounded-br-md border border-zinc-300 px-3 pb-3 pt-2 shadow-lg drop-shadow-xl transition-all duration-300 ease-in-out hover:shadow-xl" 279 | > 280 | <div class="flex items-center gap-x-1 pb-1 text-xs text-zinc-600"> 281 | <Scroll size={12} /> 282 | <span class="pb-[2.5px]"> Summary </span> 283 | </div> 284 | <span class="text-zinc-800"> 285 | {paper['summary']}, 286 | </span> 287 | </div> 288 | </div> 289 | </div> 290 | </div> 291 | </div> 292 | </div> 293 | -------------------------------------------------------------------------------- /src/routes/homepage/+page.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Navigation from '../../components/navigation.svelte'; 3 | import FeedSkeletons from '../../components/skeleton/feed_skeletons.svelte'; 4 | import Footer from '../../components/footer.svelte'; 5 | import axios from 'axios'; 6 | import { onMount } from 'svelte'; 7 | import EachPaper from '../../components/each_paper/each_paper.svelte'; 8 | import { Toaster } from 'svelte-sonner'; 9 | import { paperListState } from '../../state/papers_list.svelte'; 10 | import { inputState } from '../../state/input_state.svelte'; 11 | import { 12 | ChevronLeft, 13 | ChevronRight, 14 | CircleCheckBig, 15 | Library, 16 | Settings, 17 | Settings2 18 | } from 'lucide-svelte'; 19 | import * as Select from '$lib/components/ui/select/index.js'; 20 | import { suggestedPaperTitles } from '$lib/constants'; 21 | import { aiConversationState } from '../../state/ai_conversation_state.svelte'; 22 | import { authClient } from '$lib/auth_client'; 23 | import SelectAll from '../../components/select_all.svelte'; 24 | 25 | async function searchPaper(onPurpose: boolean = false) { 26 | if (inputState.searchContent.trim().length > 0 || onPurpose == true) { 27 | inputState.isSearching = true; 28 | inputState.statusText = `Searching for `; 29 | inputState.advancedSearch = false; 30 | paperListState.paperList = []; 31 | const response = await axios.post('/api/search_papers', { 32 | startIndex: inputState.startIndex, 33 | maxResults: inputState.maxResults, 34 | searchFilterString: { 35 | all: inputState.searchContent.replace(':', '') 36 | }, 37 | sortBy: inputState.sortBy == 'Sort By' ? 'relevance' : inputState.sortBy, 38 | sortOrder: inputState.sortOrder == 'Sort Order' ? 'ascending' : inputState.sortOrder 39 | }); 40 | paperListState.paperList = []; 41 | paperListState.paperList = response.data; 42 | inputState.lastSearch = inputState.searchContent; 43 | inputState.isSearching = false; 44 | inputState.statusText = `Results for `; 45 | } 46 | } 47 | 48 | // Constants for default values 49 | const SORT_BY_DEFAULT = 'relevance'; 50 | const SORT_ORDER_DEFAULT = 'ascending'; 51 | 52 | inputState.searchContent = ''; 53 | // inputState.lastSearch = ''; 54 | 55 | async function randomSearch(onPurpose: boolean = false): Promise<void> { 56 | try { 57 | if (inputState.lastSearch === '' || onPurpose) { 58 | inputState.statusText = 'Recommended keyword'; 59 | // Filter out current search term to avoid repetition 60 | const availableTopics = suggestedPaperTitles.filter( 61 | (topic) => topic !== inputState.lastSearch 62 | ); 63 | // If no other topics available, use current one, otherwise pick a random one from filtered list 64 | const randomTopic = 65 | availableTopics.length > 0 66 | ? availableTopics[Math.floor(Math.random() * availableTopics.length)] 67 | : suggestedPaperTitles[Math.floor(Math.random() * suggestedPaperTitles.length)]; 68 | inputState.searchContent = randomTopic; 69 | inputState.lastSearch = inputState.searchContent; 70 | inputState.isSearching = true; 71 | 72 | // Ensure we're in the browser before accessing window 73 | if (typeof window === 'undefined') { 74 | throw new Error('This function can only be called in the browser'); 75 | } 76 | 77 | // Construct the full URL 78 | const apiUrl = new URL('/api/search_papers', window.location.origin).toString(); 79 | 80 | const response = await axios.post(apiUrl, { 81 | startIndex: inputState.startIndex, 82 | maxResults: inputState.maxResults, 83 | searchFilterString: { 84 | all: inputState.searchContent 85 | }, 86 | sortBy: inputState.sortBy === 'Sort By' ? SORT_BY_DEFAULT : inputState.sortBy, 87 | sortOrder: 88 | inputState.sortOrder === 'Sort Order' ? SORT_ORDER_DEFAULT : inputState.sortOrder 89 | }); 90 | 91 | // Update the paper list directly since paperListState is not a Svelte store 92 | paperListState.paperList = response.data; 93 | inputState.lastSearch = inputState.searchContent; 94 | } 95 | } catch (error) { 96 | console.error('Failed to fetch random papers:', error); 97 | inputState.statusText = 'Failed to load recommendations. '; 98 | // You might want to show a user-friendly error message here 99 | } finally { 100 | inputState.isSearching = false; 101 | } 102 | } 103 | 104 | let isfeedControlsOn = $state(false); 105 | 106 | aiConversationState.selectedPapersList = []; 107 | aiConversationState.selectedPapersIDList = []; 108 | 109 | randomSearch(); 110 | 111 | const session = authClient.getSession(); 112 | </script> 113 | 114 | <svelte:head> 115 | <title>ScholarXIV 116 | 117 | 118 | 119 | 123 | 124 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 |
137 | 138 | 139 | 140 | 141 |
142 | 143 |
144 | 145 |
146 | 149 | 150 | 151 | randomSearch(true)} 154 | > 155 | "{inputState.lastSearch}" 156 | 157 |
158 | 159 | 160 |
161 | 162 | 163 | 164 |
(isfeedControlsOn = !isfeedControlsOn)} 167 | > 168 | 169 |
170 | 171 | 172 | 173 |
174 |
175 | 176 | 177 | {#if isfeedControlsOn} 178 |
181 |
182 | 183 | searchPaper(true)} 187 | > 188 | {inputState.sortBy} 192 | 193 | Relevance 194 | Last Updated Date 195 | Submitted Date 196 | 197 | 198 | 199 | 200 | searchPaper(true)} 204 | > 205 | {inputState.sortOrder} 209 | 210 | Ascending 211 | Descending 212 | 213 | 214 |
215 | 216 |
217 | 218 |
221 | 222 | 223 |
{ 226 | if (inputState.startIndex > 0) { 227 | inputState.startIndex -= 1; 228 | await searchPaper(true); 229 | } 230 | }} 231 | > 232 | 233 |
234 | Page {inputState.startIndex + 1} 235 | 236 | 237 |
{ 240 | inputState.startIndex += 1; 241 | await searchPaper(true); 242 | }} 243 | > 244 | 245 |
246 |
247 | 248 | 249 |
252 | 253 | 254 |
{ 257 | if (inputState.maxResults > 2) { 258 | inputState.maxResults -= 1; 259 | await searchPaper(true); 260 | } 261 | }} 262 | > 263 | 264 |
265 | Results {inputState.maxResults} 266 | 267 | 268 |
{ 271 | inputState.maxResults += 1; 272 | await searchPaper(); 273 | }} 274 | > 275 | 276 |
277 |
278 |
279 |
280 | {/if} 281 | 282 | 283 |
284 | {#if inputState.isSearching == true} 285 | 286 | {:else} 287 | {#each paperListState.paperList as eachPaper} 288 | 289 | {/each} 290 | {/if} 291 |
292 |
293 | 294 | 295 |
296 | 297 | 298 |
299 | 300 |
Showing {paperListState.paperList.length} Papers.
301 | 302 | 303 |
306 | 307 | 308 |
{ 311 | if (inputState.startIndex > 0) { 312 | inputState.startIndex -= 1; 313 | await searchPaper(true); 314 | } 315 | }} 316 | > 317 | 318 |
319 | Page {inputState.startIndex + 1} 320 | 321 | 322 |
{ 325 | inputState.startIndex += 1; 326 | await searchPaper(true); 327 | }} 328 | > 329 | 330 |
331 |
332 |
333 | 334 | 335 |
336 |
337 |
338 | 339 | 340 |
341 | 342 |
343 | 346 |
347 | --------------------------------------------------------------------------------