├── .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 |
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 |
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 |
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 |
14 |
15 |
16 |
17 |
18 |
19 | {:else if isCommentMode == false}
20 |
21 |
22 |
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 |
20 | from the arXiv repository.
25 |
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 |
24 |
25 |
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 |
72 | {@render children?.()}
73 |
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 |
83 |
84 |
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 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/components/each_paper/each_paper.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 | selectPaper()}>
37 |
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 |
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 |
90 |
91 |
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 | 
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 | |  |  |
50 | |  |  |
51 | |  |  |
52 | |  |  |
53 | |  |  |
54 | |  |  |
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 |
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 |
22 |
23 |
24 |
25 |
26 | {#if page.url.pathname == '/homepage'}
27 | {#if $session.data}
28 |
29 |
30 |
31 |
32 |
33 |
34 | {/if}
35 | {:else if page.url.pathname == '/bookmarks_page'}
36 |
37 | {#if $session.data}
38 |
39 |
40 | {/if}
41 |
42 |
43 |
44 | {:else if page.url.pathname == '/liked_papers_page'}
45 |
46 | {#if $session.data}
47 |
48 |
49 | {/if}
50 |
51 |
52 |
53 | {:else}
54 |
55 | {#if $session.data}
56 |
57 |
58 | {/if}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {/if}
67 |
68 |
69 | {#if $session.data}
70 |
71 |
72 | {#if $session.data}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | handleSignOut()}
94 | >
95 | Logout
96 |
98 |
99 |
100 |
101 | {/if}
102 |
103 | {/if}
104 |
105 |
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 |
46 |
47 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | (aiConversationState.conversation = [])}
64 | />
65 |
66 |
67 | (aiConversationState.conversation = [])}
71 | />
72 |
73 | {#if minimizeConversation == true}
74 |
75 | (minimizeConversation = !minimizeConversation)}
79 | />
80 |
81 |
82 | (minimizeConversation = !minimizeConversation)}
86 | />
87 |
88 | {:else}
89 |
90 | (minimizeConversation = !minimizeConversation)}
94 | />
95 |
96 |
97 | (minimizeConversation = !minimizeConversation)}
101 | />
102 |
103 | {/if}
104 |
105 |
106 |
107 |
108 |
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 |
128 | {/each}
129 | {:else}
130 |
144 | {/if}
145 |
146 |
147 |
--------------------------------------------------------------------------------
/src/components/each_comment/each_comment.svelte:
--------------------------------------------------------------------------------
1 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
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 |
88 |
89 |
90 |
91 |
92 |
93 |
96 |
97 | {comment.commenter.name}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
107 |
108 |
109 |
110 |
111 |
112 | {comment.commenter.name}
113 |
114 |
115 |
116 |
117 |
118 | {readableTime}
119 |
120 |
121 |
122 | {#if isCurrentUserComment}
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
136 | {#if isDeleting}
137 |
143 |
151 |
156 |
157 | Deleting...
158 | {:else}
159 |
160 | Delete
161 | {/if}
162 |
163 |
164 |
165 |
166 | {/if}
167 |
168 |
169 |
170 |
171 | {comment.comment}
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/src/components/main_input/advanced_search.svelte:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 | Advanced Search
66 | Fill out only the information you want below
67 |
68 |
69 |
70 |
71 |
72 |
77 |
78 |
79 |
80 |
85 |
86 |
87 |
88 |
89 |
90 | {inputState.sortBy}
94 |
95 | Relevance
96 | Last Updated Date
97 | Submitted Date
98 |
99 |
100 |
101 | {inputState.sortOrder}
105 |
106 | Ascending
107 | Descending
108 |
109 |
110 |
111 |
112 |
Max Result
113 |
114 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {#if inputState.isSearching == true}
131 |
138 | {:else}
139 |
140 |
141 |
advancedSearchPaper()}
144 | >
145 | Search Paper
146 |
147 | {/if}
148 |
149 |
150 |
151 |
resetAdvancedSearch()}
154 | >
155 | Reset
156 |
157 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/src/routes/auth/sign_in/+page.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 | 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 |
57 |
58 |
59 |
{
63 | isLogingInWithGoogle = !isLogingInWithGoogle;
64 | await authClient.signIn.social({
65 | provider: 'google',
66 | callbackURL: '/homepage'
67 | });
68 | }}
69 | >
70 | {#if isLogingInWithGoogle === true}
71 |
72 | {:else}
73 |
74 |
75 |
76 |
Google
77 |
78 | {/if}
79 |
80 |
81 |
82 |
{
86 | isLogingInWithGithub = !isLogingInWithGithub;
87 | await authClient.signIn.social({
88 | provider: 'github',
89 | callbackURL: '/homepage'
90 | });
91 | }}
92 | >
93 | {#if isLogingInWithGithub === true}
94 |
95 | {:else}
96 |
97 |
98 | GitHub
99 |
100 | {/if}
101 |
102 |
103 |
104 |
124 |
125 |
126 |
127 |
128 |
{
132 | isLogingInWithHuggingFace = !isLogingInWithHuggingFace;
133 | await authClient.signIn.social({
134 | provider: 'huggingface',
135 | callbackURL: '/homepage'
136 | });
137 | }}
138 | >
139 | {#if isLogingInWithHuggingFace === true}
140 |
141 | {:else}
142 |
143 |
144 |
145 | Hugging Face
146 |
147 |
148 | {/if}
149 |
150 |
151 |
152 |
{
156 | isLogingInWithAnonymous = !isLogingInWithAnonymous;
157 | await authClient.signIn.anonymous();
158 | }}
159 | >
160 | {#if isLogingInWithAnonymous === true}
161 |
162 | {:else}
163 |
164 |
165 |
166 | Guest Mode
167 |
168 | {/if}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
186 |
187 |
198 |
199 |
--------------------------------------------------------------------------------
/src/components/main_input/main_input.svelte:
--------------------------------------------------------------------------------
1 |
105 |
106 |
223 |
--------------------------------------------------------------------------------
/src/components/each_paper/interactions.svelte:
--------------------------------------------------------------------------------
1 |
73 |
74 |
75 |
76 |
81 |
82 | {#if $session.data}
83 |
84 |
85 |
{
88 | paperState.toggleLike($session.data?.user.id, paperState.paper.extractedID);
89 | e.stopPropagation();
90 | }}
91 | >
92 |
93 |
98 |
99 |
100 |
105 |
106 |
107 |
108 | {paperState.likeCount}
109 |
110 |
111 |
112 |
113 |
114 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
Comments
124 |
125 |
126 | {/if}
127 |
128 |
129 |
{
132 | paperState.toggleSummary();
133 | e.stopPropagation();
134 | }}
135 | >
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
Summary
144 |
145 |
146 |
147 |
148 |
handleDownload(paperState.paper)}
151 | >
152 |
153 |
154 |
155 |
156 |
157 |
158 |
Download
159 |
160 |
161 | {#if $session.data}
162 |
163 |
164 |
165 |
{
168 | paperState.toggleBookmark($session.data?.user.id, paperState.paper.extractedID);
169 | e.stopPropagation();
170 | }}
171 | >
172 |
173 |
178 |
179 |
180 |
185 |
186 |
187 |
188 | {paperState.isBookmarked ? 'Unbookmark' : 'Bookmark'}
189 |
190 |
191 | {/if}
192 |
193 |
194 |
195 |
196 |
197 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
Copy
207 |
208 |
209 |
210 |
211 | {
214 | event.stopPropagation();
215 | copyToClipboard('All', paperState.paper);
216 | console.log('allhere');
217 | }}>All
219 | {
222 | event.stopPropagation();
223 | copyToClipboard('ID', paperState.paper);
224 | }}>ID
226 | {
229 | event.stopPropagation();
230 | copyToClipboard('Title', paperState.paper);
231 | }}>Title
233 | {
236 | event.stopPropagation();
237 | copyToClipboard('Authors', paperState.paper);
238 | }}>Authors
240 | {
243 | event.stopPropagation();
244 | copyToClipboard('PDF Link', paperState.paper);
245 | }}>PDF Link
247 | {
250 | event.stopPropagation();
251 | copyToClipboard('Summary', paperState.paper);
252 | }}>Summary
254 | {
257 | event.stopPropagation();
258 | copyToClipboard('Published Date', paperState.paper);
259 | }}>Published Date
261 |
262 |
263 |
264 |
265 |
266 |
--------------------------------------------------------------------------------
/src/components/each_paper_old.svelte:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
84 |
293 |
--------------------------------------------------------------------------------
/src/routes/homepage/+page.svelte:
--------------------------------------------------------------------------------
1 |
113 |
114 |
115 | 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 |
147 | {inputState.statusText}
148 |
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 |
344 |
345 |
346 |
347 |
--------------------------------------------------------------------------------