├── .browserslistrc ├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .eslintrc.js ├── .github └── workflows │ ├── codeql.yml │ ├── dependency-review.yml │ └── eslint.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── images │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── connector-graph.png │ │ ├── crossref-logo.jpg │ │ ├── einstein-tounge.jpg │ │ ├── einstein.jpg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── gitlab.svg │ │ ├── graph-purple.png │ │ ├── graph.png │ │ ├── linkedin.svg │ │ ├── logo-200.png │ │ ├── logo-300.png │ │ ├── logo.png │ │ ├── network-purple.png │ │ ├── openalex-logo.png │ │ ├── opencitations-logo.png │ │ ├── profile-transparent.png │ │ ├── reddit.svg │ │ ├── semantic-scholar-logo.png │ │ ├── svgs.svg │ │ ├── twitter.svg │ │ └── unpaywall-logo.jpg │ └── tailwind.css ├── components │ ├── AbstractView.vue │ ├── Author.vue │ ├── Authors.vue │ ├── Autosuggest.vue │ ├── BetaFeatures.vue │ ├── BetaSignup.vue │ ├── ConnectorSearch.vue │ ├── ConnectorTable.vue │ ├── DashboardRenderer.vue │ ├── ExternalLinks.vue │ ├── Faq.vue │ ├── FavoritePaperButton.vue │ ├── FavoritePapers.vue │ ├── GraphFilters.vue │ ├── GraphSearch.vue │ ├── GraphView.vue │ ├── LitConnectorBody.vue │ ├── LitConnectorPaperSelector.vue │ ├── LitConnectorTour.vue │ ├── LitReviewBuilder.vue │ ├── LitReviewButton.vue │ ├── LitReviewHero.vue │ ├── Loader.vue │ ├── PaperDiscoveryEmpty.vue │ ├── PaperHero.vue │ ├── PaperHeroStats.vue │ ├── PaperList.vue │ ├── PaperPageTour.vue │ ├── PaperSummary.vue │ ├── QueryPanel.vue │ ├── SaveDropDown.vue │ ├── SearchResults.vue │ ├── SimilarGraph.vue │ ├── SqlView.vue │ ├── Stat.vue │ ├── StatView.vue │ ├── TableBase.vue │ ├── TableView.vue │ ├── Tour.vue │ ├── announcements │ │ ├── AnnouncementBanner.vue │ │ ├── MedSurveyAnnouncement.vue │ │ └── ZoteroAnnouncement.vue │ ├── layout │ │ ├── Footer.vue │ │ ├── Header.vue │ │ └── SingleColumn.vue │ └── modals │ │ ├── AuthorModalContent.vue │ │ ├── ModalManager.vue │ │ ├── NotificationModal.vue │ │ ├── PaperModalButton.vue │ │ └── PaperModalContent.vue ├── dashboard_templates │ ├── default_lr_template.yaml │ └── default_paper_template.yaml ├── main.ts ├── navigation.ts ├── router │ └── index.ts ├── shim-declarations.d.ts ├── shim-tsx.d.ts ├── shim-vue.d.ts ├── stores │ └── userStore.ts ├── types │ ├── graphTypes.ts │ ├── incitefulTypes.ts │ ├── modalTypes.ts │ ├── openAlexTypes.ts │ ├── userTypes.ts │ └── yaml.ts ├── utils │ ├── bib.ts │ ├── doi.ts │ ├── emitHelpers.ts │ ├── exportUtils.ts │ ├── graphing │ │ ├── connector.ts │ │ ├── graph.ts │ │ ├── graphStyles.ts │ │ └── similar.ts │ ├── incitefulApi.ts │ ├── keywords.ts │ ├── logging.ts │ ├── openalexApi.ts │ ├── options.ts │ ├── pagedata.ts │ └── sql.ts └── views │ ├── About.vue │ ├── BetaFeatures.vue │ ├── DataSources.vue │ ├── Einstein.vue │ ├── Export.vue │ ├── Home.vue │ ├── LitConnector.vue │ ├── LitReview.vue │ ├── LitReviewQuery.vue │ ├── NotFound.vue │ ├── PaperDiscovery.vue │ ├── PaperDiscoveryQuery.vue │ └── Search.vue ├── tailwind.config.js ├── tests └── unit │ └── example.spec.ts ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_PROJECT_ID='inciteful-xyz' 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # VUE_APP_CLIENT_API_URL=http://localhost:8080 2 | VUE_APP_CLIENT_API_URL=https://api.inciteful.xyz 3 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_SHOW_LOGIN=false -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | // '@vue/standard' 11 | ], 12 | parserOptions: { 13 | parser: '@typescript-eslint/parser' 14 | }, 15 | rules: { 16 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 18 | 'vue/multi-word-component-names': 'off', 19 | '@typescript-eslint/ban-ts-comment': 'off' 20 | }, 21 | overrides: [ 22 | { 23 | files: [ 24 | '**/__tests__/*.{j,t}s?(x)', 25 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 26 | ], 27 | env: { 28 | jest: true 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '30 19 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v2 21 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: 13 | push: 14 | branches: [ "master" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "master" ] 18 | schedule: 19 | - cron: '38 5 * * 3' 20 | 21 | jobs: 22 | build: 23 | name: Run eslint scanning 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # required for all workflows 27 | security-events: write 28 | # only required for workflows in private repositories 29 | actions: read 30 | contents: read 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version-file: '.nvmrc' 36 | 37 | - name: Install ESLint 38 | run: | 39 | npm install eslint@8.10.0 40 | npm install @microsoft/eslint-formatter-sarif@2.1.7 41 | 42 | - name: Run ESLint 43 | run: npx eslint . 44 | --config .eslintrc.js 45 | --ext .js,.jsx,.ts,.tsx 46 | --format @microsoft/eslint-formatter-sarif 47 | --output-file eslint-results.sarif 48 | continue-on-error: true 49 | 50 | - name: Upload analysis results to GitHub 51 | uses: github/codeql-action/upload-sarif@v2 52 | with: 53 | sarif_file: eslint-results.sarif 54 | wait-for-processing: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.13 -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | feedback@inciteful.xyz. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | npm-watch: 2 | npm run watch 3 | 4 | npm-build-release: 5 | npm install 6 | npm run build-prod 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inciteful Web App 2 | This is the front end of [Inciteful.xyz](https://inciteful.xyz). 3 | 4 | ## Contributing 5 | This is a personal project that I intend to keep free forever. As such, there is a lot of low hanging fruit that I have yet to get to and as a result there is a lot of opportunity for people to contribute. Feel free to reach out to me: michael@inciteful.xyz. 6 | 7 | ## Project setup 8 | ``` 9 | npm install 10 | ``` 11 | 12 | ### Compiles and hot-reloads for development 13 | ``` 14 | npm run watch 15 | ``` 16 | 17 | ### Compiles and minifies for production 18 | ``` 19 | npm run serve 20 | ``` 21 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest' 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inciteful", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": "16.x" 7 | }, 8 | "scripts": { 9 | "serve": "vue-cli-service serve --port=8081", 10 | "build": "vue-cli-service build", 11 | "test:unit": "vue-cli-service test:unit", 12 | "lint": "vue-cli-service lint", 13 | "analyze-prod": "BUNDLE_ANALYZE=true npm run build-prod", 14 | "watch": "vue-cli-service build --watch" 15 | }, 16 | "dependencies": { 17 | "@headlessui/vue": "^1.4.3", 18 | "@hennge/vue3-pagination": "^1.0.17", 19 | "@heroicons/vue": "^1.0.5", 20 | "@sentry/browser": "^6.15.0", 21 | "@sentry/tracing": "^6.15.0", 22 | "@sentry/vue": "^6.15.0", 23 | "@vueuse/head": "^1.1.19", 24 | "axios": "^0.21.2", 25 | "axios-retry": "^3.1.9", 26 | "bib2json": "0.0.1", 27 | "codejar": "^3.2.2", 28 | "core-js": "^3.6.5", 29 | "cytoscape": "^3.18.2", 30 | "cytoscape-context-menus": "^4.1.0", 31 | "cytoscape-fcose": "^2.0.0", 32 | "cytoscape-klay": "^3.1.4", 33 | "cytoscape-popper": "^2.0.0", 34 | "doi-regex": "^0.1.10", 35 | "flexsearch": "^0.7.11", 36 | "install": "^0.13.0", 37 | "mitt": "^3.0.0", 38 | "npm": "^8.11.0", 39 | "numeral": "^2.0.6", 40 | "pinia": "^2.0.9", 41 | "prismjs": "^1.27.0", 42 | "qs": "^6.10.3", 43 | "stemmer": "^2.0.0", 44 | "tippy.js": "^6.3.1", 45 | "v3-tour": "^3.0.16", 46 | "vue": "^3.2.0", 47 | "vue-router": "^4.0.0-alpha.6", 48 | "vue3-click-away": "^1.2.1", 49 | "vue3-touch-events": "^4.1.0", 50 | "zotero-api-client": "^0.37.2" 51 | }, 52 | "devDependencies": { 53 | "@sentry/webpack-plugin": "^1.18.3", 54 | "@tailwindcss/forms": "^0.5.3", 55 | "@types/cytoscape": "^3.19.2", 56 | "@types/cytoscape-popper": "^2.0.0", 57 | "@types/doi-regex": "^0.1.0", 58 | "@types/flexsearch": "^0.7.2", 59 | "@types/numeral": "^2.0.2", 60 | "@types/prismjs": "^1.16.6", 61 | "@typescript-eslint/eslint-plugin": "^5.4.0", 62 | "@typescript-eslint/parser": "^5.4.0", 63 | "@vue/cli": "^5.0.0-rc.2", 64 | "@vue/cli-plugin-babel": "^5.0.0-rc.2", 65 | "@vue/cli-plugin-eslint": "^5.0.0-rc.2", 66 | "@vue/cli-plugin-router": "^5.0.0-rc.2", 67 | "@vue/cli-plugin-typescript": "^5.0.0-rc.2", 68 | "@vue/cli-plugin-unit-jest": "^5.0.0-rc.2", 69 | "@vue/cli-service": "^5.0.0-rc.2", 70 | "@vue/compiler-sfc": "^3.0.0-beta.1", 71 | "@vue/eslint-config-typescript": "^9.1.0", 72 | "@vue/test-utils": "^2.0.0-alpha.1", 73 | "autoprefixer": "^10.4.13", 74 | "babel-eslint": "^10.1.0", 75 | "copy-webpack-plugin": "^6.1.0", 76 | "css-loader": "^6.5.1", 77 | "eslint": "^7.32.0", 78 | "eslint-plugin-import": "^2.25.3", 79 | "eslint-plugin-node": "^11.1.0", 80 | "eslint-plugin-promise": "^5.1.0", 81 | "eslint-plugin-vue": "^8.0.3", 82 | "file-loader": "^6.1.0", 83 | "js-yaml-loader": "^1.2.2", 84 | "json-loader": "^0.5.7", 85 | "mini-css-extract-plugin": "^1.2.1", 86 | "node-sass": "^9.0.0", 87 | "postcss": "^8.4.21", 88 | "postcss-loader": "^4.2.0", 89 | "sass": "^1.35.2", 90 | "sass-loader": "^10.2.0", 91 | "style-loader": "^1.3.0", 92 | "svg-url-loader": "^6.0.0", 93 | "tailwindcss": "^3.2.7", 94 | "typescript": "^4.5.4", 95 | "vue-cli-plugin-tailwind": "~2.0.6", 96 | "vue-loader": "^15.9.3", 97 | "webpack": "^5.76.0", 98 | "webpack-bundle-analyzer": "^4.1.0", 99 | "webpack-cli": "^4.9.1", 100 | "webpack-merge": "^5.2.0" 101 | }, 102 | "mode": "development" 103 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <% if (process.env.NODE_ENV==='production' ) { %> 7 | 8 | 10 | 11 | <% } %> 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | <%= htmlWebpackPlugin.options.title %> 21 | 22 | 23 | 24 | 25 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /src/assets/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/images/connector-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/connector-graph.png -------------------------------------------------------------------------------- /src/assets/images/crossref-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/crossref-logo.jpg -------------------------------------------------------------------------------- /src/assets/images/einstein-tounge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/einstein-tounge.jpg -------------------------------------------------------------------------------- /src/assets/images/einstein.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/einstein.jpg -------------------------------------------------------------------------------- /src/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/gitlab.svg: -------------------------------------------------------------------------------- 1 | 7 | tanuki 8 | 9 | -------------------------------------------------------------------------------- /src/assets/images/graph-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/graph-purple.png -------------------------------------------------------------------------------- /src/assets/images/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/graph.png -------------------------------------------------------------------------------- /src/assets/images/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/logo-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/logo-200.png -------------------------------------------------------------------------------- /src/assets/images/logo-300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/logo-300.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/network-purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/network-purple.png -------------------------------------------------------------------------------- /src/assets/images/openalex-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/openalex-logo.png -------------------------------------------------------------------------------- /src/assets/images/opencitations-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/opencitations-logo.png -------------------------------------------------------------------------------- /src/assets/images/profile-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/profile-transparent.png -------------------------------------------------------------------------------- /src/assets/images/reddit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/semantic-scholar-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/semantic-scholar-logo.png -------------------------------------------------------------------------------- /src/assets/images/svgs.svg: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /src/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/unpaywall-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inciteful-xyz/inciteful-web/34e4727c6334e97d53cce7549d3b6b4c766b3e2f/src/assets/images/unpaywall-logo.jpg -------------------------------------------------------------------------------- /src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | 7 | .base-table { 8 | @apply min-w-full divide-y divide-gray-200 table-auto text-sm text-gray-600; 9 | } 10 | .base-table thead { 11 | @apply bg-gray-50 uppercase; 12 | } 13 | .base-table th { 14 | @apply px-2 py-2 font-medium uppercase tracking-wider text-center; 15 | } 16 | .base-table tbody { 17 | @apply bg-white divide-y divide-gray-200 text-center; 18 | } 19 | .base-table tbody td { 20 | @apply px-2 py-2; 21 | } 22 | .base-button { 23 | @apply inline-flex items-center px-3 py-2 border border-transparent leading-5 font-medium rounded-md text-white focus:outline-none transition ease-in-out duration-150; 24 | } 25 | .button-gray { 26 | @apply base-button bg-gray-500 hover:bg-gray-400 focus:border-gray-600 focus:ring-gray-400; 27 | } 28 | .button-light-violet { 29 | @apply base-button bg-violet-400 30 | hover:bg-violet-300 31 | focus:border-violet-500 32 | focus:ring-violet-500; 33 | } 34 | .button-violet { 35 | @apply base-button bg-violet-600 36 | hover:bg-violet-500 37 | focus:border-violet-700 38 | focus:ring-violet-600; 39 | } 40 | h1 { 41 | @apply text-gray-800 font-bold text-lg sm:text-2xl; 42 | } 43 | 44 | h2 { 45 | @apply text-gray-800 font-bold text-base sm:text-xl; 46 | } 47 | 48 | .Pagination { 49 | @apply py-2; 50 | } 51 | .Pagination li { 52 | @apply inline; 53 | } 54 | .Pagination .Dots { 55 | @apply h-6 w-6 inline-block; 56 | } 57 | .PaginationControl .Control { 58 | @apply h-4 w-4 inline; 59 | } 60 | .PaginationControl .Control:hover { 61 | @apply cursor-pointer; 62 | } 63 | .Pagination li button { 64 | @apply p-2 py-1 border-gray-200 border rounded m-1; 65 | } 66 | .Pagination .Page-active { 67 | @apply bg-violet-500 border-violet-800 text-white; 68 | } 69 | .shadow-box { 70 | @apply shadow border-b border-gray-200 sm:rounded-lg mb-3; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/AbstractView.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 65 | -------------------------------------------------------------------------------- /src/components/Author.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | -------------------------------------------------------------------------------- /src/components/Authors.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 63 | -------------------------------------------------------------------------------- /src/components/BetaFeatures.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | -------------------------------------------------------------------------------- /src/components/BetaSignup.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /src/components/ConnectorSearch.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 70 | -------------------------------------------------------------------------------- /src/components/DashboardRenderer.vue: -------------------------------------------------------------------------------- 1 | 40 | 54 | -------------------------------------------------------------------------------- /src/components/ExternalLinks.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /src/components/Faq.vue: -------------------------------------------------------------------------------- 1 | 32 | 42 | -------------------------------------------------------------------------------- /src/components/FavoritePaperButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 51 | -------------------------------------------------------------------------------- /src/components/FavoritePapers.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 143 | -------------------------------------------------------------------------------- /src/components/GraphSearch.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 117 | -------------------------------------------------------------------------------- /src/components/LitConnectorPaperSelector.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 200 | -------------------------------------------------------------------------------- /src/components/LitConnectorTour.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 101 | -------------------------------------------------------------------------------- /src/components/LitReviewBuilder.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 88 | -------------------------------------------------------------------------------- /src/components/LitReviewButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 54 | -------------------------------------------------------------------------------- /src/components/LitReviewHero.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 73 | -------------------------------------------------------------------------------- /src/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /src/components/PaperHero.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 90 | -------------------------------------------------------------------------------- /src/components/PaperHeroStats.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 120 | -------------------------------------------------------------------------------- /src/components/PaperList.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 192 | -------------------------------------------------------------------------------- /src/components/PaperPageTour.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 129 | -------------------------------------------------------------------------------- /src/components/PaperSummary.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 37 | -------------------------------------------------------------------------------- /src/components/SaveDropDown.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 125 | -------------------------------------------------------------------------------- /src/components/SearchResults.vue: -------------------------------------------------------------------------------- 1 | 112 | 178 | -------------------------------------------------------------------------------- /src/components/SimilarGraph.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 93 | -------------------------------------------------------------------------------- /src/components/SqlView.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 112 | -------------------------------------------------------------------------------- /src/components/Stat.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 29 | -------------------------------------------------------------------------------- /src/components/StatView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | -------------------------------------------------------------------------------- /src/components/TableBase.vue: -------------------------------------------------------------------------------- 1 | 20 | 33 | -------------------------------------------------------------------------------- /src/components/Tour.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 54 | 59 | -------------------------------------------------------------------------------- /src/components/announcements/AnnouncementBanner.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /src/components/announcements/MedSurveyAnnouncement.vue: -------------------------------------------------------------------------------- 1 | 10 | 21 | -------------------------------------------------------------------------------- /src/components/announcements/ZoteroAnnouncement.vue: -------------------------------------------------------------------------------- 1 | 12 | 23 | -------------------------------------------------------------------------------- /src/components/layout/Footer.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 119 | -------------------------------------------------------------------------------- /src/components/layout/SingleColumn.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/components/modals/AuthorModalContent.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | -------------------------------------------------------------------------------- /src/components/modals/ModalManager.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 132 | -------------------------------------------------------------------------------- /src/components/modals/NotificationModal.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 104 | -------------------------------------------------------------------------------- /src/components/modals/PaperModalButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 57 | -------------------------------------------------------------------------------- /src/components/modals/PaperModalContent.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 167 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import { createPinia } from 'pinia' 5 | import * as Sentry from '@sentry/vue' 6 | import { Integrations } from '@sentry/tracing' 7 | import VueTour from 'v3-tour' 8 | import VueClickAway from 'vue3-click-away' 9 | import Vue3TouchEvents from 'vue3-touch-events' 10 | 11 | import './assets/tailwind.css' 12 | import { useUserStore } from './stores/userStore' 13 | import { emitter } from './utils/emitHelpers' 14 | import { createHead } from "@vueuse/head" 15 | 16 | require('v3-tour/dist/vue-tour.css') 17 | 18 | const app = createApp(App) 19 | 20 | // For options API 21 | app.config.globalProperties.emitter = emitter 22 | // For composition API 23 | app.provide('emitter', emitter) 24 | 25 | if (process.env.NODE_ENV === 'production') { 26 | Sentry.init({ 27 | app, 28 | dsn: 29 | 'https://e0f9638a22234f65b69a22a10537ab95@o415910.ingest.sentry.io/5512736', 30 | integrations: [new Integrations.BrowserTracing()], 31 | release: 'inciteful-js@' + process.env.__COMMIT_HASH__, 32 | environment: process.env.NODE_ENV, 33 | tracingOptions: { 34 | trackComponents: true 35 | }, 36 | tracesSampleRate: 1.0 37 | }) 38 | } 39 | 40 | (async () => { 41 | app.use(createPinia()) 42 | 43 | const head = createHead() 44 | 45 | app 46 | .use(router) 47 | .use(VueTour) 48 | .use(VueClickAway) 49 | .use(Vue3TouchEvents) 50 | .use(head) 51 | 52 | app.mount('#app') 53 | })() 54 | -------------------------------------------------------------------------------- /src/navigation.ts: -------------------------------------------------------------------------------- 1 | function getPaperUrl (id: string) { 2 | return `/p/${id}` 3 | } 4 | function getPaperQueryUrl (id: string) { 5 | return `/p/q/${id}` 6 | } 7 | 8 | export default { 9 | getPaperUrl, 10 | getPaperQueryUrl 11 | } 12 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | import pagedata from '../utils/pagedata' 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'Home', 10 | component: Home, 11 | meta: { 12 | title: 'Using Citations to Explore Academic Literature', 13 | description: 14 | 'Committed to open access, Inciteful uses the power of graph analysis to help you explore and find the most relevant academic literature.', 15 | canonical: '/' 16 | }, 17 | isSecureContext 18 | }, 19 | { 20 | path: '/about', 21 | name: 'About', 22 | // route level code-splitting 23 | // this generates a separate chunk (about.[hash].js) for this route 24 | // which is lazy-loaded when the route is visited. 25 | component: () => 26 | import(/* webpackChunkName: "about" */ '../views/About.vue'), 27 | meta: { 28 | title: 'About', 29 | description: 'About Inciteful' 30 | } 31 | }, 32 | { 33 | path: '/einstein', 34 | name: 'Einstein', 35 | component: () => 36 | import(/* webpackChunkName: "connector" */ '../views/Einstein.vue'), 37 | meta: { 38 | title: 'Six Degrees of Albert Einstein', 39 | description: 40 | "See how your work is connected to one of Einstein's seminal papers." 41 | } 42 | }, 43 | { 44 | path: '/data', 45 | name: 'Data', 46 | component: () => 47 | import(/* webpackChunkName: "data" */ '../views/DataSources.vue'), 48 | meta: { 49 | title: 'Data Sources', 50 | description: 'Where we get our data' 51 | } 52 | }, 53 | { 54 | path: '/search', 55 | name: 'Search', 56 | component: () => 57 | import(/* webpackChunkName: "search" */ '../views/Search.vue'), 58 | meta: { 59 | title: 'Search for Papers' 60 | } 61 | }, 62 | { 63 | path: '/beta', 64 | name: 'Beta', 65 | component: () => 66 | import(/* webpackChunkName: "about" */ '../views/BetaFeatures.vue'), 67 | meta: { 68 | title: 'Enable and Play with Inciteful Beta Features' 69 | } 70 | }, 71 | { 72 | path: '/p/q/:pathMatch(.*)', 73 | name: 'PaperDiscoveryQuery', 74 | component: () => 75 | import( 76 | /* webpackChunkName: "discovery" */ '../views/PaperDiscoveryQuery.vue' 77 | ), 78 | meta: { 79 | title: 'Paper Discovery Query', 80 | description: 81 | "Use our Query tool to make custom queries on the paper's graph." 82 | } 83 | }, 84 | { 85 | path: '/p/q', 86 | name: 'LitReviewQuery', 87 | component: () => 88 | import(/* webpackChunkName: "discovery" */ '../views/LitReviewQuery.vue'), 89 | meta: { 90 | title: 'Literature Review Query', 91 | description: 92 | "Use our Query tool to make custom queries on the graph you've built." 93 | } 94 | }, 95 | { 96 | path: '/p', 97 | name: 'LitReview', 98 | component: () => 99 | import(/* webpackChunkName: "discovery" */ '../views/LitReview.vue'), 100 | meta: { 101 | title: 'Literature Review', 102 | description: 103 | 'Use our Paper Discovery tool to quickly and easily find the most relevant literature.' 104 | } 105 | }, 106 | { 107 | path: '/p/:pathMatch(.*)', 108 | name: 'PaperDiscovery', 109 | component: () => 110 | import(/* webpackChunkName: "discovery" */ '../views/PaperDiscovery.vue'), 111 | meta: { 112 | title: 'Paper Discovery', 113 | description: 114 | 'Use our Paper Discovery tool to quickly and easily find the most relevant literature.' 115 | } 116 | }, 117 | { 118 | path: '/c', 119 | name: 'LitConnector', 120 | component: () => 121 | import(/* webpackChunkName: "connector" */ '../views/LitConnector.vue'), 122 | meta: { 123 | title: 'Literature Connector', 124 | description: 125 | 'Use our Literature Connector to Discover How Two Papers are Connected' 126 | } 127 | }, 128 | { 129 | path: '/e', 130 | name: 'ToolExport', 131 | component: () => 132 | import(/* webpackChunkName: "connector" */ '../views/Export.vue'), 133 | meta: { 134 | title: 'Export', 135 | description: 'Export your search to Mendeley or Zotero' 136 | } 137 | }, 138 | { 139 | path: '/:pathMatch(.*)*', 140 | name: 'not-found', 141 | component: () => 142 | import(/* webpackChunkName: "about" */ '../views/NotFound.vue'), 143 | meta: { 144 | title: 'Page Not Found' 145 | } 146 | }, 147 | // if you omit the last `*`, the `/` character in params will be encoded when resolving or pushing 148 | { 149 | path: '/:pathMatch(.*)', 150 | name: 'bad-not-found', 151 | component: () => 152 | import(/* webpackChunkName: "about" */ '../views/NotFound.vue'), 153 | meta: { 154 | title: 'Page Not Found' 155 | } 156 | } 157 | ] 158 | 159 | const router = createRouter({ 160 | history: createWebHistory(process.env.BASE_URL), 161 | routes, 162 | 163 | scrollBehavior(_to, _from, savedPosition) { 164 | if (savedPosition) { 165 | return savedPosition 166 | } else { 167 | return { left: 0, top: 0 } 168 | } 169 | }, 170 | // @ts-ignore 171 | parseQuery: qs.parse, 172 | stringifyQuery: function (params) { 173 | const result = qs.stringify(params, { 174 | arrayFormat: 'brackets' 175 | }) 176 | return result || '' 177 | } 178 | }) 179 | 180 | router.afterEach(to => { 181 | pagedata.setTitle(to.meta.title as string) 182 | pagedata.setDescription(to.meta.description as string) 183 | }) 184 | 185 | export default router 186 | -------------------------------------------------------------------------------- /src/shim-declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'bib2json' { 2 | import bib2json from 'bib2json' 3 | export default bib2json 4 | } 5 | declare module 'zotero-api-client' { 6 | import api from 'zotero-api-client' 7 | export default api 8 | } 9 | -------------------------------------------------------------------------------- /src/shim-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | type Element = VNode 7 | // tslint:disable no-empty-interface 8 | type ElementClass = Vue 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shim-vue.d.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | declare module '*.vue' { 3 | import Vue from 'vue' 4 | export default Vue 5 | } 6 | declare module '@vue/runtime-core' { 7 | interface ComponentCustomProperties { 8 | emitter: mitt; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/stores/userStore.ts: -------------------------------------------------------------------------------- 1 | 2 | import { defineStore } from 'pinia' 3 | import { UserData } from '../types/userTypes'; 4 | import { PaperID } from '../types/incitefulTypes'; 5 | 6 | export const useUserStore = defineStore({ 7 | id: 'loggedInUser', 8 | state: () => { 9 | return { 10 | userDataDoc: undefined as UserData | undefined, 11 | enabled: process.env.VUE_APP_SHOW_LOGIN == "true" 12 | } 13 | }, 14 | actions: { 15 | async saveUser(user: UserData) { 16 | throw new Error("Not implemented") 17 | }, 18 | async awaitUserDataLoad(f: () => void) { 19 | if (this.isSignedIn) { 20 | if (this.userDataDoc) { 21 | f() 22 | } else { 23 | setTimeout(() => this.awaitUserDataLoad(f), 100) 24 | } 25 | } 26 | }, 27 | async addFavorite(id: PaperID) { 28 | await this.addFavorites([id]) 29 | }, 30 | async addFavorites(ids: PaperID[]) { 31 | this.awaitUserDataLoad(async () => { 32 | if (this.userDataDoc) { 33 | throw new Error("Not implemented") 34 | } 35 | }) 36 | }, 37 | async removeFavorite(id: PaperID) { 38 | this.awaitUserDataLoad(async () => { 39 | if (this.userDataDoc) { 40 | throw new Error("Not implemented") 41 | } 42 | }) 43 | }, 44 | async toggleFavorite(id: PaperID) { 45 | if (!this.isPaperFavorite(id)) 46 | this.addFavorite(id) 47 | else 48 | this.removeFavorite(id) 49 | } 50 | }, 51 | getters: { 52 | userData(state) { 53 | return state.userDataDoc 54 | }, 55 | isSignedIn(state): boolean { 56 | return false 57 | }, 58 | userName(state): string | undefined { 59 | throw new Error("Not implemented") 60 | }, 61 | userId(state): string | undefined { 62 | throw new Error("Not implemented") 63 | }, 64 | initial(): string | undefined { 65 | return (this.userName && this.userName.length > 0 ? this.userName[0].toUpperCase() : undefined) 66 | }, 67 | isPaperFavorite() { 68 | return (id: PaperID) => { 69 | if (this.userData) 70 | return this.userData.favoritePapers.some(x => x == id) 71 | else return false 72 | } 73 | }, 74 | 75 | }, 76 | }) -------------------------------------------------------------------------------- /src/types/graphTypes.ts: -------------------------------------------------------------------------------- 1 | import { LayoutOptions, Core } from 'cytoscape'; 2 | import { Paper, Connection, Path, PaperID } from './incitefulTypes'; 3 | import { IModalOptions } from './modalTypes'; 4 | 5 | 6 | export interface GraphModalOptions extends IModalOptions { 7 | connectTo?: PaperID; 8 | } 9 | 10 | export interface GraphData { 11 | type: string; 12 | papers?: Paper[]; 13 | connections?: Connection[]; 14 | paths?: Path[]; 15 | toId?: PaperID; 16 | fromId?: PaperID; 17 | sourcePaperIds?: PaperID[]; 18 | modalOptions?: GraphModalOptions; 19 | } 20 | 21 | export class IncitefulGraph { 22 | cy: Core; 23 | sourcePaperIds?: PaperID[]; 24 | constructor(cy: Core, sourcePaperIds?: PaperID[]) { 25 | this.cy = cy; 26 | this.sourcePaperIds = sourcePaperIds; 27 | this.cy.on('resize', () => { 28 | this.centerSource(); 29 | }); 30 | 31 | this.cy.ready(() => { 32 | this.centerSource(); 33 | }); 34 | } 35 | 36 | centerSource() { 37 | const sourcePaperId = this.sourcePaperIds && this.sourcePaperIds.length == 1 ? this.sourcePaperIds[0] : undefined; 38 | if (this.cy && (this.cy.height() > 600 || !sourcePaperId)) { 39 | this.cy.fit(); 40 | } else { 41 | const j = this.cy.$(`[id = "${sourcePaperId}"]`); 42 | this.cy.reset().center(j); 43 | } 44 | } 45 | 46 | filterNodes(ids: Set) { 47 | this.cy.nodes().forEach(node => { 48 | if (ids.has(node.id())) { 49 | node.removeClass('disabled'); 50 | } else { 51 | node.addClass('disabled'); 52 | } 53 | }); 54 | } 55 | 56 | highlightNodes(ids: Set) { 57 | this.cy.nodes().forEach(node => { 58 | if (ids.has(node.id())) { 59 | node.addClass('highlighted'); 60 | } else { 61 | node.removeClass('highlighted'); 62 | } 63 | }); 64 | } 65 | 66 | resize() { 67 | this.cy.resize(); 68 | } 69 | 70 | zoom(level?: number) { 71 | return this.cy.zoom(level); 72 | } 73 | 74 | curZoom(): number { 75 | return this.cy.zoom(); 76 | } 77 | 78 | renderLayout(layoutParams: LayoutOptions) { 79 | const layout = this.cy.layout(layoutParams); 80 | layout.run(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/types/incitefulTypes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | import { Emitter, EventType } from 'mitt' 4 | 5 | export type IncitefulEmitter = Emitter> 6 | 7 | export interface IIndexable { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | [key: string]: any 10 | } 11 | 12 | export type PaperID = string 13 | 14 | export enum ReferenceManagers { 15 | Zotero = 'Zotero', 16 | Mendeley = 'Mendeley', 17 | EndNote = 'EndNote', 18 | RefWorks = 'RefWorks' 19 | } 20 | 21 | export interface PaperAutosuggest { 22 | id: PaperID 23 | title: string 24 | authors: string 25 | num_cited_by: number 26 | } 27 | 28 | function authorListDisplay(a: Author[] | undefined): string { 29 | if (!a) return '' 30 | 31 | let display = a 32 | .slice(0, 3) 33 | .map(x => x.name) 34 | .join(', ') 35 | 36 | if (a.length > 3) display += ', et al' 37 | 38 | return display 39 | } 40 | 41 | export function paperIntoPaperAutosuggest(p: Paper): PaperAutosuggest { 42 | return { 43 | id: p.id, 44 | title: p.title || 'NA', 45 | authors: authorListDisplay(p.author), 46 | num_cited_by: p.num_cited_by 47 | } 48 | } 49 | 50 | export interface Paper { 51 | id: PaperID 52 | doi?: string 53 | author: Author[] 54 | title: string 55 | published_year: number 56 | journal?: string 57 | pages: string 58 | volume: string 59 | num_citing: number 60 | num_cited_by: number 61 | distance?: number 62 | path_count?: number 63 | citing?: PaperID[] 64 | cited_by?: PaperID[] 65 | } 66 | 67 | export interface LockedPaper extends Paper { 68 | isLocked: boolean 69 | } 70 | 71 | export interface Author { 72 | author_id?: number 73 | name: string 74 | sequence: number 75 | position?: string 76 | institution?: Institution 77 | } 78 | 79 | export interface Institution { 80 | id?: number 81 | name: string 82 | } 83 | 84 | export interface PaperConnector { 85 | source: PaperID 86 | papers: Paper[] 87 | paths: Path[] 88 | connections: Connection[] 89 | num_paths: number 90 | papers_searched: number 91 | max_hops: number 92 | } 93 | export interface ExternalIds { 94 | DOI?: string | null 95 | PMID?: string | null 96 | PMCID?: string | null 97 | } 98 | export interface Connection { 99 | citing: string 100 | cited: string 101 | } 102 | 103 | export type Path = PaperID[] 104 | 105 | export type QueryValue = string | number | Author[] 106 | 107 | export type QueryRow = Record 108 | 109 | export type QueryResults = QueryRow[] 110 | 111 | export interface Faq { 112 | question: string 113 | answer: string 114 | } 115 | -------------------------------------------------------------------------------- /src/types/modalTypes.ts: -------------------------------------------------------------------------------- 1 | import { PaperID, Author } from './incitefulTypes'; 2 | 3 | export interface IModalOptions { 4 | previousScreen?: ModalOptions; 5 | } 6 | 7 | export interface PaperModalOptions extends IModalOptions { 8 | paperId: PaperID; 9 | connectTo?: PaperID; 10 | } 11 | 12 | export interface AuthorModalOptions extends IModalOptions { 13 | author: Author; 14 | contextIds?: PaperID[]; 15 | } 16 | 17 | export interface SaveCollectionAction { 18 | contextIds: PaperID[]; 19 | } 20 | 21 | export type CollectionModalActions = SaveCollectionAction; 22 | 23 | export function isSaveCollectionAction( 24 | options: CollectionModalActions 25 | ): options is SaveCollectionAction { 26 | return (options as SaveCollectionAction).contextIds !== undefined; 27 | } 28 | 29 | 30 | export type ModalOptions = PaperModalOptions | AuthorModalOptions; 31 | 32 | export function isPaperModalOptons( 33 | options: ModalOptions 34 | ): options is PaperModalOptions { 35 | return (options as PaperModalOptions).paperId !== undefined; 36 | } 37 | 38 | export function isAuthorModalOptions( 39 | options: ModalOptions 40 | ): options is AuthorModalOptions { 41 | return (options as AuthorModalOptions).author !== undefined; 42 | } 43 | 44 | export interface NotificationModalOptions { 45 | message1: string, 46 | message2: string 47 | } -------------------------------------------------------------------------------- /src/types/openAlexTypes.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface OAAutosuggestResponse { 3 | meta: Meta 4 | results: OAAutosuggestResult[] 5 | } 6 | 7 | export interface Meta { 8 | count: number 9 | db_response_time_ms: number 10 | page: number 11 | per_page: number 12 | } 13 | 14 | 15 | export interface OAAutosuggestResult { 16 | id: string; 17 | display_name: string; 18 | hint: string; 19 | cited_by_count: number; 20 | works_count: number; 21 | entityType: string; 22 | externalId: string; 23 | } 24 | 25 | 26 | export interface OAPaperSearchResults { 27 | meta: Meta 28 | results: OAPaper[] 29 | } 30 | 31 | 32 | export interface OAPaper { 33 | id: string 34 | doi?: string 35 | title?: string 36 | display_name?: string 37 | publication_year?: number 38 | publication_date?: string 39 | ids: OAWorkIds 40 | primary_location: OAPrimaryLocation 41 | type?: string 42 | open_access: OAOpenAccess 43 | authorships: OAAuthorship[] 44 | cited_by_count: number 45 | biblio: OABiblio 46 | is_retracted: boolean 47 | is_paratext?: boolean 48 | concepts?: OAConcept[] 49 | mesh?: OAMesh[] 50 | referenced_works: string[] 51 | related_works: string[] 52 | cited_by_api_url: string 53 | updated_date: string 54 | created_date: string 55 | } 56 | 57 | export interface OAWorkIds { 58 | openalex?: string 59 | doi?: string 60 | mag?: string 61 | pmid?: string 62 | pmcid?: string 63 | } 64 | 65 | export interface OAPrimaryLocation { 66 | is_oa: boolean 67 | landing_page_url: string 68 | pdf_url: string 69 | source: OALocationSource 70 | license: string 71 | version: string 72 | is_accepted: boolean 73 | is_published: boolean 74 | } 75 | 76 | export interface OALocationSource { 77 | id: string 78 | display_name: string 79 | issn_l: string 80 | issn: string[] 81 | is_oa: boolean 82 | is_in_doaj: boolean 83 | host_organization: string 84 | host_organization_name: string 85 | host_organization_lineage: string[] 86 | host_organization_lineage_names: string[] 87 | type: string 88 | } 89 | 90 | export interface OAOpenAccess { 91 | is_oa: boolean 92 | oa_status: string 93 | oa_url: string 94 | } 95 | 96 | export interface OAMesh { 97 | descriptor_ui: string 98 | descriptor_name: string 99 | qualifier_ui: string 100 | qualifier_name?: string 101 | is_major_topic: boolean 102 | } 103 | 104 | export interface OAAuthorship { 105 | author_position: string 106 | author: OAAuthor 107 | institutions: OAInstitution[] 108 | raw_affiliation_string: string 109 | } 110 | 111 | export interface OAAuthor { 112 | id: string 113 | display_name: string 114 | orcid?: string 115 | } 116 | 117 | export interface OAInstitution { 118 | id?: string 119 | display_name: string 120 | ror: string 121 | country_code: string 122 | type: string 123 | } 124 | 125 | export interface OABiblio { 126 | volume: string 127 | issue: string 128 | first_page: string 129 | last_page: string 130 | } 131 | 132 | export interface OAConcept { 133 | id: string 134 | wikidata: string 135 | display_name: string 136 | level: number 137 | score: string 138 | } 139 | 140 | -------------------------------------------------------------------------------- /src/types/userTypes.ts: -------------------------------------------------------------------------------- 1 | import { PaperID } from "./incitefulTypes"; 2 | 3 | export interface UserData { 4 | id: string, 5 | favoritePapers: PaperID[] 6 | } 7 | 8 | export enum ItemVisibility { 9 | Hidden = "HIDDEN", 10 | Public = "PUBLIC", 11 | } 12 | 13 | export interface IncitefulCollection { 14 | id: string | null, 15 | ownerId: string, 16 | parentID: string | null, 17 | visibility: ItemVisibility, 18 | name: string, 19 | papers: IncitefulCollectionItem[] 20 | dateCreated: Date 21 | } 22 | 23 | export enum IncitefulCollectionItemSource { 24 | Inciteful = "INCITEFUL", 25 | Zotero = "ZOTERO", 26 | } 27 | export interface IncitefulCollectionItem { 28 | paperId: PaperID, 29 | dateAdded: Date, 30 | source: IncitefulCollectionItemSource, 31 | } 32 | -------------------------------------------------------------------------------- /src/types/yaml.ts: -------------------------------------------------------------------------------- 1 | declare module '*.yaml' { 2 | const data: any 3 | export default data 4 | } -------------------------------------------------------------------------------- /src/utils/bib.ts: -------------------------------------------------------------------------------- 1 | import bib2json from 'bib2json' 2 | 3 | function idsFromBib(bibText: string) { 4 | const results = bib2json(bibText) 5 | const ids: string[] = [] 6 | results.entries.forEach( 7 | (element: { 8 | Fields: { incitefulid: string; magid: string; doi: string; url: string; URL: string; DOI: string }; 9 | }) => { 10 | if (element.Fields.incitefulid) { 11 | ids.push(element.Fields.incitefulid) 12 | } else if (element.Fields.magid) { 13 | ids.push(`MAG:${element.Fields.magid}`) 14 | } else if (element.Fields.doi) { 15 | ids.push(element.Fields.doi) 16 | } else if (element.Fields.DOI) { 17 | ids.push(element.Fields.DOI) 18 | } 19 | 20 | if (element.Fields.url) { 21 | ids.push(element.Fields.url) 22 | } 23 | if (element.Fields.URL) { 24 | ids.push(element.Fields.URL) 25 | } 26 | } 27 | ) 28 | 29 | return ids 30 | } 31 | 32 | export default { 33 | idsFromBib 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/doi.ts: -------------------------------------------------------------------------------- 1 | import doiRegex from 'doi-regex' 2 | 3 | function isExactDoi(doi: string) { 4 | if (doi.startsWith('http://doi.org/')) { 5 | doi = doi.slice('http://doi.org/'.length) 6 | } 7 | 8 | return !doi ? false : doiRegex({ exact: true }).test(doi) 9 | } 10 | 11 | 12 | function buildDoi(doi: string): string | undefined { 13 | if (!doi) return undefined 14 | 15 | if (doi.startsWith('http://doi.org/')) { 16 | return doi 17 | } else { 18 | if (!isExactDoi(doi)) 19 | return undefined 20 | 21 | if (doi.startsWith('doi:')) { 22 | doi = doi.slice('doi:'.length) 23 | } 24 | 25 | return `https://doi.org/${doi}` 26 | } 27 | } 28 | 29 | export default { 30 | isExactDoi, 31 | buildDoi 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/emitHelpers.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | import { IncitefulEmitter, PaperID } from '../types/incitefulTypes'; 3 | import { ModalOptions, NotificationModalOptions } from '../types/modalTypes'; 4 | 5 | export const EmitEvents = { 6 | ShowModal: Symbol("show_modal"), 7 | AddToLitReview: Symbol("add_to_lit_review"), 8 | ShowNotification: Symbol("show_notification"), 9 | GoToPaper: Symbol("go_to_paper"), 10 | GraphLoaded: Symbol("graph_loaded"), 11 | } 12 | 13 | export const emitter = mitt() as IncitefulEmitter 14 | 15 | export const showModalHelper = (modalOptions: ModalOptions) => { 16 | emitter.emit(EmitEvents.ShowModal, modalOptions) 17 | } 18 | 19 | export const showNotificationHelper = (options: NotificationModalOptions) => { 20 | emitter.emit(EmitEvents.ShowNotification, options) 21 | } 22 | 23 | export const graphLoadedHelper = () => { 24 | emitter.emit(EmitEvents.GraphLoaded) 25 | } 26 | export const addToLitReviewHelper = (id: PaperID) => { 27 | emitter.emit(EmitEvents.AddToLitReview, id) 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/exportUtils.ts: -------------------------------------------------------------------------------- 1 | import { Paper } from '../types/incitefulTypes'; 2 | import { useHead } from '@vueuse/head'; 3 | 4 | export function sendDataUpdated() { 5 | document.dispatchEvent( 6 | new Event('ZoteroItemUpdated', { 7 | bubbles: true, 8 | cancelable: true 9 | }) 10 | ) 11 | } 12 | 13 | export function setExportHeaders(papers: Paper[]) { 14 | if (!papers) return 15 | 16 | const meta: { name: string; content: string | undefined }[] = []; 17 | papers.forEach((paper: Paper) => { 18 | if (paper.doi) { 19 | meta.push({ 20 | name: 'citation_doi', 21 | content: paper.doi 22 | }) 23 | } 24 | }) 25 | 26 | useHead({ meta }) 27 | 28 | setTimeout(() => { 29 | sendDataUpdated() 30 | }, 500) 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/graphing/graph.ts: -------------------------------------------------------------------------------- 1 | import cytoscape, { Core, NodeSingular, ElementDefinition } from 'cytoscape' 2 | // @ts-ignore 3 | import contextMenus from 'cytoscape-context-menus' 4 | import 'cytoscape-context-menus/cytoscape-context-menus.css' 5 | // @ts-ignore 6 | import fcose from 'cytoscape-fcose' 7 | // @ts-ignore 8 | import klay from 'cytoscape-klay' 9 | import graphStyles from './graphStyles' 10 | import similar from './similar' 11 | import connector from './connector' 12 | import popper from 'cytoscape-popper' 13 | import tippy from 'tippy.js' 14 | import 'tippy.js/dist/tippy.css' 15 | import { 16 | PaperID 17 | } from '@/types/incitefulTypes' 18 | import { 19 | GraphData, 20 | IncitefulGraph, 21 | GraphModalOptions 22 | } from "@/types/graphTypes" 23 | import { Emitter } from 'mitt' 24 | import { PaperModalOptions } from "../../types/modalTypes" 25 | import { showModalHelper } from '../emitHelpers'; 26 | import { IIndexable } from '../../types/incitefulTypes'; 27 | 28 | 29 | cytoscape.use(popper) 30 | cytoscape.use(fcose) 31 | cytoscape.use(klay) 32 | cytoscape.use(contextMenus) 33 | 34 | function hideTippy(node: NodeSingular) { 35 | const tippy = node.data('tippy') 36 | 37 | if (tippy != null) { 38 | tippy.hide() 39 | } 40 | } 41 | 42 | function createTippys(cy: Core) { 43 | cy.nodes().forEach(node => { 44 | const content = node.data('tippyContent') 45 | 46 | if (content) { 47 | const ref = node.popperRef() // used only for positioning 48 | 49 | // A dummy element must be passed as tippy only accepts dom element(s) as the target 50 | // https://atomiks.github.io/tippyjs/v6/constructor/#target-types 51 | const dummyDomEle = document.createElement('div') 52 | 53 | const tip = tippy(dummyDomEle, { 54 | // tippy props: 55 | getReferenceClientRect: ref.getBoundingClientRect, // https://atomiks.github.io/tippyjs/v6/all-props/#getreferenceclientrect 56 | trigger: 'manual', // mandatory, we cause the tippy to show programmatically. 57 | arrow: true, 58 | allowHTML: true, 59 | hideOnClick: false, 60 | interactive: false, 61 | appendTo: document.body, 62 | // your own custom props 63 | // content prop can be used when the target is a single element https://atomiks.github.io/tippyjs/v6/constructor/#prop 64 | content 65 | }) 66 | 67 | node.data('tippy', tip) 68 | const els: PaperID[] = node.data('elsToHighlight') 69 | 70 | node.on('mouseover', function () { 71 | // If the context menu is open, don't trigger the mouseover actions. 72 | if ( 73 | cy.scratch && 74 | cy.scratch().cycontextmenus && 75 | cy.scratch().cycontextmenus.cxtMenu && 76 | cy.scratch().cycontextmenus.cxtMenu.style.display !== 'none' 77 | ) { 78 | return 79 | } 80 | 81 | tip.show() 82 | setTimeout(() => tip.hide(), 5000) 83 | 84 | if (els) { 85 | els.forEach((id: PaperID) => { 86 | const el = cy.$(`[id = "${id}"]`) 87 | if (el) el.addClass('highlighted') 88 | }) 89 | } 90 | 91 | node.addClass('highlighted') 92 | cy.nodes() 93 | .not(node) 94 | .forEach(hideTippy) 95 | }) 96 | 97 | node.on('mouseout', function () { 98 | if (els) { 99 | els.forEach(id => { 100 | const el = cy.$(`[id = "${id}"]`) 101 | if (el) el.removeClass('highlighted') 102 | }) 103 | } 104 | 105 | node.removeClass('highlighted') 106 | tip.hide() 107 | }) 108 | } 109 | }) 110 | } 111 | function setupTippy(cy: Core, modalOptions: GraphModalOptions) { 112 | createTippys(cy) 113 | 114 | const hideAllTippies = function () { 115 | cy.nodes().forEach(hideTippy) 116 | } 117 | 118 | cy.on('tap', 'node', function (ev) { 119 | hideAllTippies() 120 | const id = ev.target.data('id') 121 | if (id) { 122 | (modalOptions as PaperModalOptions).paperId = id 123 | showModalHelper(modalOptions as PaperModalOptions) 124 | } 125 | }) 126 | 127 | cy.on('tap', 'edge', function () { 128 | hideAllTippies() 129 | }) 130 | 131 | cy.on('zoom pan', function () { 132 | hideAllTippies() 133 | }) 134 | } 135 | 136 | function loadBaseGraph( 137 | elements: ElementDefinition[], 138 | container: HTMLElement, 139 | modalOptions: GraphModalOptions 140 | ) { 141 | const cy = cytoscape({ 142 | container, 143 | autounselectify: true, 144 | userZoomingEnabled: true, 145 | userPanningEnabled: true, 146 | elements, 147 | style: graphStyles.default, 148 | layout: { name: 'random' } 149 | }) 150 | 151 | setupTippy(cy, modalOptions) 152 | 153 | return cy 154 | } 155 | 156 | function loadGraph( 157 | graphData: GraphData, 158 | container: HTMLElement, 159 | bus: Emitter>, 160 | minDate: number, 161 | maxDate: number 162 | ) { 163 | let elements 164 | let layoutParams 165 | let contextMenuOptions 166 | 167 | if (graphData.type === 'similar') { 168 | elements = similar.buildElements(graphData, minDate, maxDate) 169 | layoutParams = similar.buildLayout() 170 | } 171 | 172 | if (graphData.type === 'connector') { 173 | elements = connector.buildElements(graphData, minDate, maxDate) 174 | layoutParams = connector.buildLayout() 175 | contextMenuOptions = connector.contextMenu(bus) 176 | } 177 | 178 | if (elements === undefined) { 179 | console.error('Graph type not supported: ' + graphData.type) 180 | return new IncitefulGraph(cytoscape()) 181 | } 182 | 183 | const cy = loadBaseGraph( 184 | elements, 185 | container, 186 | graphData.modalOptions ?? {} 187 | ) 188 | 189 | const graph = new IncitefulGraph(cy, graphData.sourcePaperIds) 190 | 191 | // Context menus are not working on Safari 192 | // https://github.com/iVis-at-Bilkent/cytoscape.js-context-menus/issues/55 193 | const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) 194 | if (contextMenuOptions && !isSafari) { 195 | const cyany = cy as IIndexable 196 | 197 | if (cyany && cyany.contextMenus) 198 | cyany.contextMenus(contextMenuOptions) 199 | } 200 | 201 | // @ts-ignore 202 | graph.renderLayout(layoutParams) 203 | 204 | return graph 205 | } 206 | 207 | export default { 208 | loadGraph 209 | } 210 | -------------------------------------------------------------------------------- /src/utils/graphing/graphStyles.ts: -------------------------------------------------------------------------------- 1 | import { EdgeSingular, Stylesheet } from 'cytoscape' 2 | 3 | const style: Stylesheet[] = [ 4 | { 5 | selector: 'node', 6 | style: { 7 | label: 'data(title)', 8 | width: 'data(size)', 9 | height: 'data(size)', 10 | color: 'white', 11 | 'background-color': 'data(color)', 12 | 'border-width': 2, 13 | 'border-color': 'white', 14 | 'text-outline-width': 6, 15 | 'text-outline-color': 'data(color)', 16 | 'font-size': '18px', 17 | 'text-valign': 'center', 18 | 'text-halign': 'center', 19 | 'overlay-padding': '6px' 20 | } 21 | }, 22 | { 23 | selector: 'node[group=\'isSource\']', 24 | style: { 25 | 'z-index': 1, 26 | 'border-width': '9px', 27 | 'border-color': '#AAD8FF', 28 | 'border-opacity': 0.7 29 | } 30 | }, 31 | { 32 | selector: 'node.disabled', 33 | style: { 34 | 'background-color': '#9fa6b2', 35 | 'text-outline-color': '#9fa6b2' 36 | } 37 | }, 38 | { 39 | selector: 'edge', 40 | style: { 41 | 'curve-style': 'straight', 42 | color: 'data(color)', 43 | 'overlay-padding': '2px', 44 | opacity: (edge: EdgeSingular) => { return Number(edge.data('opacity')) }, 45 | width: 2 46 | } 47 | }, 48 | { 49 | selector: '.highlighted', 50 | style: { 51 | 'z-index': 999999 52 | } 53 | }, 54 | { 55 | selector: 'node.highlighted', 56 | style: { 57 | 'border-width': '12px', 58 | 'border-color': '#AAD8FF', 59 | 'border-opacity': 0.7 60 | } 61 | }, 62 | { 63 | selector: 'edge.highlighted', 64 | style: { 65 | width: '4px', 66 | 'line-color': '#AAD8FF', 67 | opacity: 1 68 | } 69 | }, 70 | { 71 | selector: 'node:selected', 72 | style: { 73 | 'border-width': '6px', 74 | 'border-color': '#AAD8FF', 75 | 'border-opacity': 0.5, 76 | 'background-color': '#77828C', 77 | 'text-outline-color': '#77828C' 78 | } 79 | } 80 | ] 81 | 82 | export default { 83 | default: style 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/keywords.ts: -------------------------------------------------------------------------------- 1 | import { Paper } from '@/types/incitefulTypes' 2 | import { stemmer } from 'stemmer' 3 | 4 | const stop = new Set([ 5 | 'new', 6 | 'i', 7 | 'me', 8 | 'my', 9 | 'myself', 10 | 'we', 11 | 'our', 12 | 'ours', 13 | 'ourselves', 14 | 'you', 15 | 'your', 16 | 'yours', 17 | 'yourself', 18 | 'yourselves', 19 | 'he', 20 | 'him', 21 | 'his', 22 | 'himself', 23 | 'she', 24 | 'her', 25 | 'hers', 26 | 'herself', 27 | 'it', 28 | 'its', 29 | 'itself', 30 | 'they', 31 | 'them', 32 | 'their', 33 | 'theirs', 34 | 'themselves', 35 | 'what', 36 | 'which', 37 | 'who', 38 | 'whom', 39 | 'this', 40 | 'that', 41 | 'these', 42 | 'those', 43 | 'am', 44 | 'is', 45 | 'are', 46 | 'was', 47 | 'were', 48 | 'be', 49 | 'been', 50 | 'being', 51 | 'have', 52 | 'has', 53 | 'had', 54 | 'having', 55 | 'do', 56 | 'does', 57 | 'did', 58 | 'doing', 59 | 'a', 60 | 'an', 61 | 'the', 62 | 'and', 63 | 'but', 64 | 'if', 65 | 'or', 66 | 'because', 67 | 'as', 68 | 'until', 69 | 'while', 70 | 'of', 71 | 'at', 72 | 'by', 73 | 'for', 74 | 'with', 75 | 'about', 76 | 'based', 77 | 'against', 78 | 'between', 79 | 'into', 80 | 'through', 81 | 'during', 82 | 'before', 83 | 'after', 84 | 'above', 85 | 'below', 86 | 'to', 87 | 'from', 88 | 'up', 89 | 'down', 90 | 'in', 91 | 'out', 92 | 'on', 93 | 'off', 94 | 'over', 95 | 'under', 96 | 'again', 97 | 'further', 98 | 'then', 99 | 'once', 100 | 'here', 101 | 'there', 102 | 'when', 103 | 'where', 104 | 'why', 105 | 'how', 106 | 'all', 107 | 'any', 108 | 'both', 109 | 'each', 110 | 'few', 111 | 'more', 112 | 'most', 113 | 'other', 114 | 'some', 115 | 'such', 116 | 'no', 117 | 'nor', 118 | 'not', 119 | 'only', 120 | 'own', 121 | 'same', 122 | 'so', 123 | 'than', 124 | 'too', 125 | 'very', 126 | 's', 127 | 't', 128 | 'can', 129 | 'will', 130 | 'just', 131 | 'don', 132 | 'should', 133 | 'now' 134 | ]) 135 | 136 | export interface TermCount { 137 | term: string; 138 | count: number; 139 | } 140 | 141 | function extract(papers: Paper[]): TermCount[] { 142 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 143 | const keywords: Record = {} 144 | papers.forEach(p => { 145 | const title = p.title.replace(/\W/g, ' ').toLowerCase() 146 | new Set(title.split(' ')).forEach(k => { 147 | if (k && !stop.has(k) && k.length > 1) { 148 | keywords[k] ? keywords[k]++ : (keywords[k] = 1) 149 | } 150 | }) 151 | }) 152 | 153 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 154 | const stemmedKeywords: Record = {} 155 | Object.entries(keywords).forEach(k => { 156 | const sk = stemmer(k[0]) 157 | if (!stemmedKeywords[sk]) { 158 | const tc: TermCount = { term: k[0], count: k[1] } 159 | stemmedKeywords[sk] = tc 160 | } else { 161 | stemmedKeywords[sk].count = stemmedKeywords[sk].count + k[1] 162 | } 163 | }) 164 | 165 | const groupedKeywords = Object.entries(stemmedKeywords) 166 | .map(k => k[1]) 167 | .filter(k => k.count > 1) 168 | 169 | groupedKeywords.sort((a, b) => b.count - a.count) 170 | return groupedKeywords.slice(0, 15) 171 | } 172 | 173 | export default { 174 | extract 175 | } 176 | -------------------------------------------------------------------------------- /src/utils/logging.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/browser' 2 | import { AxiosError } from 'axios' 3 | 4 | export function logError(err: AxiosError) { 5 | Sentry.setExtra('error', err) 6 | Sentry.captureException(err) 7 | } 8 | 9 | export function logInfo(message: string, obj: unknown) { 10 | Sentry.withScope(scope => { 11 | Sentry.setExtra('obj', obj) 12 | Sentry.captureMessage(message, scope) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/openalexApi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OAAuthorship, 3 | OAAutosuggestResult, 4 | OAAutosuggestResponse, 5 | OAPaper, 6 | OAPaperSearchResults 7 | } from '../types/openAlexTypes' 8 | import { Author, PaperAutosuggest, Paper } from '../types/incitefulTypes' 9 | import axios, { AxiosError, AxiosResponse } from 'axios' 10 | import { logError } from './logging' 11 | import doiHelpers from './doi' 12 | 13 | function handleServiceErr(err: AxiosError) { 14 | if (err && err.response && err.response.status !== 404) { 15 | logError(err) 16 | } 17 | } 18 | 19 | export function searchOpenAlex(query: string): Promise { 20 | if (query == null || query == undefined || query == "") 21 | return Promise.resolve([]) 22 | 23 | const doi = doiHelpers.buildDoi(query) 24 | if (doi) { 25 | return getOAPaper(doi).then(p => { 26 | if (p) 27 | return [convertOAPaperToPaper(p)] 28 | else 29 | return Promise.resolve([]) 30 | }) 31 | } else { 32 | //remove non alphanumeric characters 33 | query = query.replace(/[^a-zA-Z0-9 ]/g, ' ') 34 | return titleSearch(query).then(papers => { 35 | if (papers.length > 0) 36 | return papers 37 | 38 | return fullSearch(query).then(papers => { 39 | if (papers.length > 0) 40 | return papers 41 | 42 | return [] 43 | }) 44 | }) 45 | } 46 | } 47 | 48 | function titleSearch(query: string): Promise { 49 | return genericSearch(query, 'https://api.openalex.org/works?filter=title.search:') 50 | } 51 | 52 | function fullSearch(query: string): Promise { 53 | return genericSearch(query, 'https://api.openalex.org/works?search=') 54 | } 55 | 56 | function genericSearch(query: string, searchUrl: string): Promise { 57 | return axios 58 | .get( 59 | `${searchUrl}${encodeURIComponent( 60 | query 61 | )}&mailto=info@inciteful.xyz` 62 | ) 63 | .then((res: AxiosResponse) => { 64 | if (res.data && res.data.results) { 65 | const results = res.data.results 66 | .map(convertOAPaperToPaper) 67 | 68 | const cited_results = results 69 | .filter(p => p.num_cited_by > 0 || p.num_citing > 0) 70 | 71 | if (cited_results.length == 0) 72 | return results 73 | else 74 | return cited_results 75 | } else { 76 | return Promise.reject() 77 | } 78 | }) 79 | .catch(err => { 80 | handleServiceErr(err) 81 | return Promise.reject() 82 | }) 83 | } 84 | 85 | function convertOAPaperToPaper(p: OAPaper): Paper { 86 | try { 87 | const newP = { 88 | id: trimOAUrl(p.id), 89 | title: p.title || 'NA', 90 | author: p.authorships.map(convertOAAuthorToAuthor), 91 | published_year: p.publication_year || 1900, 92 | journal: p.primary_location?.source?.display_name, 93 | num_cited_by: p.cited_by_count, 94 | num_citing: p.referenced_works.length, 95 | doi: p.doi, 96 | pages: p.biblio.first_page + '-' + p.biblio.last_page, 97 | volume: p.biblio.volume 98 | } 99 | return newP 100 | } catch (e) { 101 | console.log(e) 102 | throw e 103 | } 104 | } 105 | 106 | function convertOAAuthorToAuthor(a: OAAuthorship, index: number): Author { 107 | return { 108 | author_id: a.author.id ? parseInt(trimOAUrl(a.author.id).slice(1)) : undefined, 109 | name: a.author.display_name, 110 | institution: 111 | !a.institutions || a.institutions.length == 0 112 | ? undefined 113 | : { 114 | id: !a.institutions[0].id 115 | ? undefined 116 | : parseInt(trimOAUrl(a.institutions[0].id).slice(1)), 117 | name: a.institutions[0].display_name 118 | }, 119 | sequence: index 120 | } 121 | } 122 | 123 | function trimOAUrl(url: string): string { 124 | return url.replace('https://openalex.org/', '') 125 | } 126 | 127 | export function searchOAAutocomplete( 128 | query: string 129 | ): Promise { 130 | if (query) { 131 | return axios 132 | .get( 133 | `https://api.openalex.org/autocomplete/works?q=${encodeURIComponent( 134 | query 135 | )}&mailto=info@inciteful.xyz` 136 | ) 137 | .then((res: AxiosResponse) => { 138 | if (res.data && res.data.results && res.data.results.length > 0) { 139 | return res.data.results.map(convertOAAutocomplete) 140 | } else { 141 | return Promise.reject() 142 | } 143 | }) 144 | .catch(err => { 145 | handleServiceErr(err) 146 | return Promise.reject() 147 | }) 148 | } 149 | return Promise.resolve([]) 150 | } 151 | 152 | function convertOAAutocomplete(p: OAAutosuggestResult): PaperAutosuggest { 153 | return { 154 | id: trimOAUrl(p.id), 155 | title: p.display_name, 156 | authors: p.hint, 157 | num_cited_by: p.cited_by_count 158 | } 159 | } 160 | 161 | export function getOAPaper(id: string): Promise { 162 | if (id) { 163 | return axios 164 | .get( 165 | `https://api.openalex.org/works/${encodeURIComponent( 166 | id 167 | )}?mailto=info@inciteful.xyz` 168 | ) 169 | .then((res: AxiosResponse) => { 170 | return res.data 171 | }) 172 | .catch(err => { 173 | if (err.response && err.response.status == 404) { 174 | return Promise.resolve(undefined) 175 | } 176 | 177 | handleServiceErr(err) 178 | return Promise.reject() 179 | }) 180 | } 181 | 182 | return Promise.resolve(undefined) 183 | } 184 | 185 | export function getOAPapers(ids: string[]): Promise { 186 | //example endpoint: https://api.openalex.org/works?filter=openalex:W4224016882|W4223895588 187 | if (ids && ids.length > 0) { 188 | return axios 189 | .get( 190 | `https://api.openalex.org/works?filter=openalex:${ids 191 | .map(id => `${id}`) 192 | .join('|')}&mailto=info@inciteful.xyz` 193 | ) 194 | .then((res: AxiosResponse) => { 195 | return res.data.results 196 | }) 197 | .catch(err => { 198 | handleServiceErr(err) 199 | return Promise.reject() 200 | }) 201 | } 202 | 203 | return Promise.resolve([]) 204 | } 205 | -------------------------------------------------------------------------------- /src/utils/options.ts: -------------------------------------------------------------------------------- 1 | const pruneKey = 'pruneLevel' 2 | 3 | function getPruneLevel (): number | undefined { 4 | const val = parseInt(localStorage[pruneKey]) 5 | return val || undefined 6 | } 7 | 8 | function setPruneLevel (val: number | undefined) { 9 | if (val) { 10 | localStorage.setItem(pruneKey, val.toString()) 11 | } else { 12 | localStorage.removeItem(pruneKey) 13 | } 14 | } 15 | 16 | export default { 17 | getPruneLevel, 18 | setPruneLevel 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/pagedata.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_TITLE = 'Inciteful' 2 | 3 | function setTitle (title: string) { 4 | document.title = (title || DEFAULT_TITLE) + ' | Inciteful.xyz' 5 | } 6 | function setDescription (description: string) { 7 | if (document != null) { 8 | const d = document.querySelector('meta[name="description"]') 9 | 10 | if (d != null) { 11 | d.setAttribute('content', description) 12 | } 13 | } 14 | } 15 | 16 | export default { 17 | setTitle, 18 | setDescription 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/sql.ts: -------------------------------------------------------------------------------- 1 | interface SqlFilter { 2 | sql: string, 3 | default?: number | undefined 4 | } 5 | 6 | const filterMap: Record = { 7 | minYear: { 8 | sql: 'p.published_year >= {}' 9 | }, 10 | maxYear: { 11 | sql: 'p.published_year <= {}' 12 | }, 13 | minDistance: { 14 | sql: 'p.distance >= {}', 15 | default: 1 16 | }, 17 | maxDistance: { 18 | sql: 'p.distance <= {}' 19 | }, 20 | keywords: { 21 | sql: 'p.paper_id IN (SELECT paper_id FROM title_search(\'{}\'))' 22 | } 23 | } 24 | 25 | function getSqlForKey(k: string, value: string | undefined) { 26 | const val = value || filterMap[k].default 27 | 28 | if (val != undefined) { 29 | return filterMap[k].sql.replace('{}', val.toString()) 30 | } 31 | 32 | return undefined 33 | } 34 | 35 | function constructFilterSql(filters: Record) { 36 | const sqlArray = Object.keys(filterMap) 37 | .map((k) => getSqlForKey(k, filters[k])) 38 | .filter((v) => v) 39 | 40 | return sqlArray.join('\nAND ') 41 | } 42 | 43 | function addFilters(sql: string, filters: Record) { 44 | while (sql.includes('{{filters}}')) { 45 | sql = _addFilters(sql, filters) 46 | } 47 | 48 | return sql 49 | } 50 | function _addFilters(sql: string, filters: Record) { 51 | if (!sql) { 52 | return sql 53 | } 54 | 55 | const pos = sql.indexOf('{{filters}}') 56 | 57 | if (pos === -1) { 58 | return sql 59 | } 60 | 61 | const filtSql = constructFilterSql(filters) 62 | 63 | return sql.replace('{{filters}}', filtSql) 64 | } 65 | 66 | export default { 67 | addFilters 68 | } 69 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 72 | 81 | 82 | SingleColumn 83 | -------------------------------------------------------------------------------- /src/views/BetaFeatures.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 113 | -------------------------------------------------------------------------------- /src/views/DataSources.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 106 | 107 | SingleColumn 108 | -------------------------------------------------------------------------------- /src/views/Export.vue: -------------------------------------------------------------------------------- 1 | 40 | 79 | -------------------------------------------------------------------------------- /src/views/LitReview.vue: -------------------------------------------------------------------------------- 1 | 24 | 84 | -------------------------------------------------------------------------------- /src/views/LitReviewQuery.vue: -------------------------------------------------------------------------------- 1 | 11 | 28 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 15 | 16 | SingleColumn 17 | -------------------------------------------------------------------------------- /src/views/PaperDiscovery.vue: -------------------------------------------------------------------------------- 1 | 66 | 162 | -------------------------------------------------------------------------------- /src/views/PaperDiscoveryQuery.vue: -------------------------------------------------------------------------------- 1 | 12 | 70 | -------------------------------------------------------------------------------- /src/views/Search.vue: -------------------------------------------------------------------------------- 1 | 32 | 80 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.html', './src/**/*.vue'], 3 | theme: { 4 | extend: { 5 | animation: { 6 | 'bounce-fade': 'bounce 5s 5s' 7 | } 8 | } 9 | }, 10 | plugins: [require('@tailwindcss/forms')] 11 | } 12 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | // import { shallowMount } from '@vue/test-utils' 2 | // import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | // describe('HelloWorld.vue', () => { 5 | // it('renders props.msg when passed', () => { 6 | // const msg = 'new message' 7 | // const wrapper = shallowMount(HelloWorld, { 8 | // propsData: { msg } 9 | // }) 10 | // expect(wrapper.text()).toMatch(msg) 11 | // }) 12 | // }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strictPropertyInitialization": false, 13 | "resolveJsonModule": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": ["webpack-env"], 17 | "paths": { 18 | "@/*": ["src/*"] 19 | }, 20 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 21 | }, 22 | "include": [ 23 | "src/**/*.ts", 24 | "src/**/*.tsx", 25 | "src/**/*.vue", 26 | "tests/**/*.ts", 27 | "tests/**/*.tsx" 28 | ], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: config => { 3 | config.module 4 | .rule('yaml') 5 | .test(/\.yaml$/) 6 | .use('js-yaml-loader') 7 | .loader('js-yaml-loader') 8 | .end() 9 | }, 10 | configureWebpack: { 11 | devtool: 'source-map' 12 | } 13 | } 14 | --------------------------------------------------------------------------------