├── src ├── vite-env.d.ts ├── recrec-api │ ├── .prettierrc │ ├── worker-configuration.d.ts │ ├── .editorconfig │ ├── package.json │ ├── src │ │ └── index.ts │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── wrangler.toml │ └── tsconfig.json ├── images │ ├── icon-caret-down.svg │ ├── icon-bars.svg │ ├── icon-external-link.svg │ ├── icon-person.svg │ ├── icon-check.svg │ ├── icon-cross.svg │ ├── icon-hindex-c.svg │ ├── icon-search.svg │ ├── icon-file-c.svg │ ├── icon-hindex.svg │ ├── icon-file.svg │ ├── icon-cite-times-c.svg │ ├── icon-cite-times.svg │ ├── icon-click.svg │ ├── icon-gear.svg │ └── recrec-logo.svg ├── components │ ├── header-bar │ │ ├── header-bar.css │ │ └── header-bar.ts │ ├── slider │ │ ├── slider.css │ │ └── slider.ts │ ├── author-view │ │ ├── author-view.css │ │ └── author-view.ts │ ├── page │ │ ├── page.ts │ │ └── page.css │ ├── author-list │ │ ├── author-list.css │ │ └── author-list.ts │ ├── recrec │ │ ├── recrec.css │ │ └── recrec.ts │ ├── paper-view │ │ ├── paper-view.css │ │ └── paper-view.ts │ └── recommender-view │ │ └── recommender-view.css ├── types │ └── common-types.ts ├── api │ └── semantic-scholar.ts └── config │ └── config.ts ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-70x70.png ├── apple-touch-icon.png ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── favicon_package_v0.16.zip ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── site.webmanifest ├── safari-pinned-tab.svg └── global.css ├── .prettierrc ├── .fttemplates └── default-template │ ├── [FTName].css │ └── [FTName].ts ├── .gitignore ├── CITATION.cff ├── tsconfig.json ├── .github └── workflows │ └── build.yml ├── LICENSE ├── package.json ├── vite.config.ts ├── .eslintrc.cjs ├── index.html └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/mstile-70x70.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/mstile-144x144.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/mstile-310x150.png -------------------------------------------------------------------------------- /public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/mstile-310x310.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon_package_v0.16.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/favicon_package_v0.16.zip -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaohk/recrec/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/recrec-api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": false 6 | } 7 | -------------------------------------------------------------------------------- /src/recrec-api/worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler 2 | // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` 3 | interface Env { 4 | } 5 | -------------------------------------------------------------------------------- /src/images/icon-caret-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.fttemplates/default-template/[FTName].css: -------------------------------------------------------------------------------- 1 | . { 2 | width: 100%; 3 | height: 100%; 4 | 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: center; 8 | align-items: center; 9 | 10 | box-sizing: border-box; 11 | } 12 | -------------------------------------------------------------------------------- /src/recrec-api/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | pnpm-lock.yaml 27 | .vercel 28 | notebooks -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | abstract: Recommender for recommender letter writers 2 | authors: 3 | - family-names: Wang 4 | given-names: Zijie J. 5 | orcid: 0000-0003-4360-1423 6 | cff-version: 1.2.0 7 | date-released: '2024-07-09' 8 | doi: 10.5281/zenodo.12697229 9 | license: 10 | - mit 11 | repository-code: https://github.com/xiaohk/recrec/tree/v0.0.3 12 | title: 'RecRec: Recommender for Recommender Letter Writers' 13 | type: software 14 | version: v0.0.3 15 | -------------------------------------------------------------------------------- /src/recrec-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recrec-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "devDependencies": { 12 | "@cloudflare/workers-types": "^4.20240701.0", 13 | "itty-router": "^3.0.12", 14 | "typescript": "^5.5.2", 15 | "wrangler": "^3.60.3" 16 | } 17 | } -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/images/icon-bars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/icon-external-link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | "types": [], 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": false, 21 | "noUnusedParameters": false, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "exclude": ["src/recrec-api"] 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the main branch 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | #: Run the test every week 10 | schedule: 11 | - cron: '0 12 * * 1' 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16] 20 | os: [ubuntu-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Install dependencies 28 | run: npm install 29 | - name: Build 30 | run: npm run build 31 | -------------------------------------------------------------------------------- /src/images/icon-person.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/icon-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/icon-cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/icon-hindex-c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024, Jay Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/images/icon-file-c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/icon-hindex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recrec", 3 | "private": false, 4 | "version": "0.0.4", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --port 3000", 8 | "build": "tsc && vite build", 9 | "build:github": "tsc && vite build --mode github", 10 | "deploy:github": "pnpm run build:github && pnpm gh-pages -m \"Deploy $(git log '--format=format:%H' main -1)\" -d ./dist", 11 | "deploy:vercel": "pnpm run build && cp -r .vercel ./dist/ && vercel ./dist", 12 | "preview": "vite preview" 13 | }, 14 | "devDependencies": { 15 | "@floating-ui/dom": "^1.6.5", 16 | "@shoelace-style/shoelace": "^2.15.1", 17 | "@types/d3-format": "^3.0.4", 18 | "@typescript-eslint/eslint-plugin": "^6.21.0", 19 | "@xiaohk/utils": "^0.0.7", 20 | "d3-format": "^3.1.0", 21 | "eslint": "^8.57.0", 22 | "eslint-config-prettier": "^9.1.0", 23 | "eslint-plugin-lit": "^1.14.0", 24 | "eslint-plugin-prettier": "^5.1.3", 25 | "eslint-plugin-wc": "^2.1.0", 26 | "gh-pages": "^6.1.1", 27 | "lit": "^3.1.4", 28 | "prettier": "^3.3.1", 29 | "typescript": "^5.4.5", 30 | "vite": "^5.2.12", 31 | "vite-plugin-dts": "^3.9.1", 32 | "vite-plugin-web-components-hmr": "^0.1.3", 33 | "wrangler": "^3.64.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/recrec-api/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to Cloudflare Workers! This is your first worker. 3 | * 4 | * - Run `npm run dev` in your terminal to start a development server 5 | * - Open a browser tab at http://localhost:8787/ to see your worker in action 6 | * - Run `npm run deploy` to publish your worker 7 | * 8 | * Bind resources to your worker in `wrangler.toml`. After adding bindings, a type definition for the 9 | * `Env` object can be regenerated with `npm run cf-typegen`. 10 | * 11 | * Learn more at https://developers.cloudflare.com/workers/ 12 | */ 13 | 14 | export interface Env { 15 | SEMANTIC_API: string; 16 | } 17 | 18 | export default { 19 | async fetch(request, env, ctx): Promise { 20 | const url = new URL(request.url); 21 | const proxyUrl = url.searchParams.get('proxyUrl'); // get a query param value (?proxyUrl=...) 22 | 23 | if (!proxyUrl) { 24 | return new Response('Bad request: Missing `proxyUrl` query param', { status: 400 }); 25 | } 26 | 27 | const newRequest = new Request(request, { 28 | headers: { 29 | 'x-api-key': `${env.SEMANTIC_API}`, 30 | }, 31 | body: request.body, 32 | }); 33 | 34 | // make subrequests with the global `fetch()` function 35 | const res = await fetch(proxyUrl, newRequest); 36 | 37 | return res; 38 | }, 39 | } satisfies ExportedHandler; 40 | -------------------------------------------------------------------------------- /src/images/icon-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import dts from 'vite-plugin-dts'; 4 | import { hmrPlugin, presets } from 'vite-plugin-web-components-hmr'; 5 | 6 | export default defineConfig(({ command, mode }) => { 7 | if (command === 'serve') { 8 | // Development 9 | return { 10 | plugins: [ 11 | hmrPlugin({ 12 | include: ['./src/**/*.ts'], 13 | presets: [presets.lit] 14 | }) 15 | ] 16 | }; 17 | } else if (command === 'build') { 18 | switch (mode) { 19 | case 'production': { 20 | // Production: standard web page (default mode) 21 | return { 22 | build: { 23 | outDir: 'dist', 24 | rollupOptions: { 25 | input: { 26 | main: resolve(__dirname, 'index.html') 27 | } 28 | } 29 | }, 30 | plugins: [] 31 | }; 32 | } 33 | 34 | case 'github': { 35 | // Production: github page (default mode) 36 | return { 37 | base: '/recrec/', 38 | build: { 39 | outDir: 'dist', 40 | rollupOptions: { 41 | input: { 42 | main: resolve(__dirname, 'index.html') 43 | } 44 | } 45 | }, 46 | plugins: [] 47 | }; 48 | } 49 | 50 | default: { 51 | console.error(`Error: unknown production mode ${mode}`); 52 | return null; 53 | } 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/header-bar/header-bar.css: -------------------------------------------------------------------------------- 1 | .header-bar { 2 | width: 100%; 3 | padding: 10px var(--container-h-padding); 4 | 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: space-between; 8 | align-items: center; 9 | 10 | box-sizing: border-box; 11 | background-color: var(--blue-900); 12 | color: white; 13 | 14 | .svg-icon { 15 | width: 24px; 16 | height: 24px; 17 | } 18 | } 19 | 20 | button { 21 | all: unset; 22 | cursor: pointer; 23 | 24 | &[disabled] { 25 | cursor: no-drop; 26 | opacity: 0.5; 27 | } 28 | } 29 | 30 | .move-pre { 31 | transform: rotate(90deg); 32 | } 33 | 34 | .move-next { 35 | transform: rotate(-90deg); 36 | } 37 | 38 | .svg-icon { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | width: 1em; 43 | height: 1em; 44 | 45 | color: currentColor; 46 | transition: transform 80ms linear; 47 | transform-origin: center; 48 | 49 | & svg { 50 | fill: currentColor; 51 | width: 100%; 52 | height: 100%; 53 | } 54 | } 55 | 56 | .title-middle { 57 | display: flex; 58 | align-items: center; 59 | font-weight: 600; 60 | font-size: var(--font-u2); 61 | text-align: center; 62 | gap: 10px; 63 | cursor: default; 64 | 65 | .step-info { 66 | font-variant-numeric: tabular-nums; 67 | white-space: nowrap; 68 | 69 | border-radius: 5px; 70 | padding: 0px 8px; 71 | background-color: hsla(0, 0%, 100%, 0.15); 72 | } 73 | } 74 | 75 | .title-right { 76 | display: flex; 77 | align-items: center; 78 | gap: 10px; 79 | } 80 | 81 | @media only screen and (max-width: 768px) { 82 | .title-middle { 83 | padding: 0 20px; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/images/icon-cite-times-c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/images/icon-cite-times.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/images/icon-click.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/icon-gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | root: true, 19 | parser: '@typescript-eslint/parser', 20 | extends: [ 21 | 'eslint:recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 24 | 'plugin:wc/recommended', 25 | 'plugin:lit/recommended', 26 | 'prettier' 27 | ], 28 | parserOptions: { 29 | ecmaVersion: 'latest', 30 | sourceType: 'module', 31 | tsconfigRootDir: __dirname, 32 | project: ['./tsconfig.json', './tsconfig.node.json'], 33 | extraFileExtensions: ['.cjs'] 34 | }, 35 | env: { 36 | es6: true, 37 | browser: true 38 | }, 39 | plugins: ['lit', '@typescript-eslint', 'prettier'], 40 | ignorePatterns: ['node_modules'], 41 | rules: { 42 | indent: 'off', 43 | 'linebreak-style': ['error', 'unix'], 44 | quotes: ['error', 'single', { avoidEscape: true }], 45 | 'prefer-const': ['error'], 46 | semi: ['error', 'always'], 47 | // 'max-len': [ 48 | // 'warn', 49 | // { 50 | // code: 80 51 | // } 52 | // ], 53 | 'no-constant-condition': ['error', { checkLoops: false }], 54 | 'prettier/prettier': 2, 55 | '@typescript-eslint/ban-ts-comment': 'off', 56 | '@typescript-eslint/restrict-template-expressions': 'off', 57 | '@typescript-eslint/no-non-null-assertion': 'off', 58 | '@typescript-eslint/no-empty-function': 'off', 59 | '@typescript-eslint/no-unnecessary-type-assertion': 'off', 60 | '@typescript-eslint/no-unused-vars': [ 61 | 'warn', 62 | { 63 | argsIgnorePattern: '^_', 64 | varsIgnorePattern: '^_', 65 | caughtErrorsIgnorePattern: '^_' 66 | } 67 | ], 68 | 'no-self-assign': 'off' 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/recrec-api/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | root: true, 19 | parser: '@typescript-eslint/parser', 20 | extends: [ 21 | 'eslint:recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 24 | 'plugin:wc/recommended', 25 | 'plugin:lit/recommended', 26 | 'prettier' 27 | ], 28 | parserOptions: { 29 | ecmaVersion: 'latest', 30 | sourceType: 'module', 31 | tsconfigRootDir: __dirname, 32 | project: ['./tsconfig.json', './tsconfig.node.json'], 33 | extraFileExtensions: ['.cjs'] 34 | }, 35 | env: { 36 | es6: true, 37 | browser: true 38 | }, 39 | plugins: ['lit', '@typescript-eslint', 'prettier'], 40 | ignorePatterns: ['node_modules'], 41 | rules: { 42 | indent: 'off', 43 | 'linebreak-style': ['error', 'unix'], 44 | quotes: ['error', 'single', { avoidEscape: true }], 45 | 'prefer-const': ['error'], 46 | semi: ['error', 'always'], 47 | // 'max-len': [ 48 | // 'warn', 49 | // { 50 | // code: 80 51 | // } 52 | // ], 53 | 'no-constant-condition': ['error', { checkLoops: false }], 54 | 'prettier/prettier': 2, 55 | '@typescript-eslint/ban-ts-comment': 'off', 56 | '@typescript-eslint/restrict-template-expressions': 'off', 57 | '@typescript-eslint/no-non-null-assertion': 'off', 58 | '@typescript-eslint/no-empty-function': 'off', 59 | '@typescript-eslint/no-unnecessary-type-assertion': 'off', 60 | '@typescript-eslint/no-unused-vars': [ 61 | 'warn', 62 | { 63 | argsIgnorePattern: '^_', 64 | varsIgnorePattern: '^_', 65 | caughtErrorsIgnorePattern: '^_' 66 | } 67 | ], 68 | 'no-self-assign': 'off' 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/slider/slider.css: -------------------------------------------------------------------------------- 1 | .slider { 2 | --thumb-width: 14px; 3 | 4 | width: 100%; 5 | 6 | height: 5px; 7 | 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | box-sizing: border-box; 14 | 15 | position: relative; 16 | top: 1px; 17 | 18 | &[align-inner] { 19 | width: calc(100% - var(--thumb-width)); 20 | margin-left: calc(var(--thumb-width) / 2); 21 | margin-right: calc(var(--thumb-width) / 2); 22 | } 23 | } 24 | 25 | .slider-track { 26 | position: absolute; 27 | top: 0px; 28 | left: 0px; 29 | 30 | width: 100%; 31 | height: 100%; 32 | border-radius: 5px; 33 | display: flex; 34 | 35 | &.background-track { 36 | background: var(--background-color); 37 | } 38 | 39 | &.range-track { 40 | width: 0px; 41 | border-bottom-right-radius: 0; 42 | border-top-right-radius: 0; 43 | background: var(--foreground-color); 44 | } 45 | } 46 | 47 | .middle-thumb { 48 | position: absolute; 49 | z-index: 3; 50 | top: 50%; 51 | left: calc(var(--thumb-width) / -2); 52 | transform: translateY(-50%); 53 | 54 | width: var(--thumb-width); 55 | height: var(--thumb-width); 56 | border-radius: 100%; 57 | 58 | background-color: var(--thumb-color); 59 | color: var(--thumb-color); 60 | box-shadow: 61 | 0px 2px 1px -1px hsla(0, 0%, 0%, 0.2), 62 | 0px 1px 1px 0px hsla(0, 0%, 0%, 0.14), 63 | 0px 1px 3px 0px hsla(0, 0%, 0%, 0.08); 64 | 65 | cursor: grab; 66 | 67 | &::before { 68 | content: ''; 69 | display: inline-block; 70 | position: absolute; 71 | z-index: 1; 72 | width: 5px; 73 | height: 5px; 74 | border-radius: 50%; 75 | background: currentColor; 76 | opacity: 0.2; 77 | left: 50%; 78 | top: 50%; 79 | margin-top: -2.5px; 80 | margin-left: -2.5px; 81 | -webkit-backface-visibility: hidden; 82 | -webkit-transform: translateZ(0); 83 | transform: scale(0.1); 84 | transition: transform 300ms ease-in-out; 85 | } 86 | 87 | &:hover { 88 | &::before { 89 | transform: scale(5); 90 | } 91 | 92 | .thumb-label { 93 | transition-delay: 200ms; 94 | transform: translateX(-50%) translateY(100%) scale(1); 95 | } 96 | } 97 | 98 | &:focus { 99 | cursor: grabbing; 100 | outline: none; 101 | 102 | &::before { 103 | transform: scale(7); 104 | } 105 | 106 | .thumb-label { 107 | transform: translateX(-50%) translateY(100%) scale(1); 108 | } 109 | } 110 | 111 | &.animating { 112 | .thumb-label { 113 | transform: translateX(-50%) translateY(100%) scale(1); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.fttemplates/default-template/[FTName].ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | 5 | import componentCSS from './.css?inline'; 6 | 7 | /** 8 | * [FTName | sentencecase] element. 9 | */ 10 | @customElement('recrec-[FTName]') 11 | export class RecRec extends LitElement { 12 | //==========================================================================|| 13 | // Class Properties || 14 | //==========================================================================|| 15 | 16 | //==========================================================================|| 17 | // Lifecycle Methods || 18 | //==========================================================================|| 19 | constructor() { 20 | super(); 21 | } 22 | 23 | /** 24 | * This method is called when the DOM is added for the first time 25 | */ 26 | firstUpdated() {} 27 | 28 | /** 29 | * This method is called before new DOM is updated and rendered 30 | * @param changedProperties Property that has been changed 31 | */ 32 | willUpdate(changedProperties: PropertyValues) {} 33 | 34 | //==========================================================================|| 35 | // Custom Methods || 36 | //==========================================================================|| 37 | async initData() {}; 38 | 39 | //==========================================================================|| 40 | // Event Handlers || 41 | //==========================================================================|| 42 | 43 | //==========================================================================|| 44 | // Private Helpers || 45 | //==========================================================================|| 46 | 47 | //==========================================================================|| 48 | // Templates and Styles || 49 | //==========================================================================|| 50 | render() { 51 | return html`
[FTName | sentencecase]
`; 52 | }; 53 | 54 | static styles = [ 55 | css` 56 | ${unsafeCSS(componentCSS)} 57 | ` 58 | ]; 59 | } 60 | 61 | declare global { 62 | interface HTMLElementTagNameMap { 63 | 'recrec-': RecRec; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/recrec-api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /src/components/author-view/author-view.css: -------------------------------------------------------------------------------- 1 | .author-view { 2 | width: 100%; 3 | height: 100%; 4 | min-height: 0px; 5 | padding: var(--container-v-padding) var(--container-h-padding) 0 6 | var(--container-h-padding); 7 | 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: space-between; 11 | align-items: center; 12 | 13 | box-sizing: border-box; 14 | --sl-input-focus-ring-color: var(--blue-100); 15 | } 16 | 17 | .content-container { 18 | box-sizing: border-box; 19 | width: 100%; 20 | height: 100%; 21 | min-height: 0px; 22 | 23 | display: flex; 24 | flex-flow: column; 25 | } 26 | 27 | .search-result { 28 | width: calc(100% - 4px); 29 | align-self: center; 30 | max-height: 90%; 31 | min-height: 0px; 32 | overflow: auto; 33 | 34 | border: 1px solid var(--gray-300); 35 | border-top: none; 36 | 37 | box-shadow: 0 2px 8px hsla(0, 0%, 0%, 0.08); 38 | 39 | &[is-hidden] { 40 | display: none; 41 | } 42 | } 43 | 44 | .svg-icon { 45 | display: inline-flex; 46 | justify-content: center; 47 | align-items: center; 48 | width: 1em; 49 | height: 1em; 50 | 51 | color: currentColor; 52 | transition: transform 80ms linear; 53 | transform-origin: center; 54 | 55 | & svg { 56 | fill: currentColor; 57 | width: 100%; 58 | height: 100%; 59 | } 60 | } 61 | 62 | .search-icon { 63 | color: var(--gray-700); 64 | } 65 | 66 | .footer { 67 | border-top: 1px solid var(--gray-300); 68 | padding: var(--container-v-padding) var(--container-h-padding); 69 | width: 100%; 70 | display: flex; 71 | flex-flow: row; 72 | justify-content: flex-end; 73 | } 74 | 75 | button { 76 | all: unset; 77 | } 78 | 79 | .confirm-button { 80 | border-radius: 5px; 81 | border: 1px solid var(--blue-600); 82 | background: var(--blue-600); 83 | color: white; 84 | font-weight: 500; 85 | 86 | padding: 8px 16px; 87 | box-sizing: border-box; 88 | position: relative; 89 | cursor: pointer; 90 | 91 | transition: 92 | background linear 100ms, 93 | border linear 100ms; 94 | 95 | &:disabled { 96 | cursor: no-drop; 97 | border: 1px solid var(--gray-300); 98 | color: var(--gray-600); 99 | background: var(--gray-100); 100 | } 101 | 102 | &:not(:disabled) { 103 | &:hover { 104 | border: 1px solid color-mix(in lab, var(--blue-600), white 10%); 105 | background-color: color-mix(in lab, var(--blue-600), white 10%); 106 | } 107 | 108 | &:active { 109 | background: var(--blue-600); 110 | border: 1px solid var(--blue-600); 111 | } 112 | } 113 | } 114 | 115 | .loader { 116 | --stroke: 2px; 117 | 118 | width: 128px; 119 | aspect-ratio: 1; 120 | border-radius: 50%; 121 | 122 | background: 123 | radial-gradient(farthest-side, var(--blue-700) 94%, #0000) top/var(--stroke) 124 | var(--stroke) no-repeat, 125 | conic-gradient(#0000 30%, var(--blue-700)); 126 | 127 | -webkit-mask: radial-gradient( 128 | farthest-side, 129 | #0000 calc(100% - var(--stroke)), 130 | #000 0 131 | ); 132 | animation: rotation 1200ms infinite linear; 133 | 134 | &[is-hidden] { 135 | visibility: hidden; 136 | pointer-events: none; 137 | } 138 | } 139 | 140 | @keyframes rotation { 141 | 100% { 142 | transform: rotate(1turn); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | RecRec | Recommender for Recommendation Letter Writers 15 | 19 | 24 | 25 | 26 | 27 | 28 | 32 | 37 | 38 | 39 | 40 | 41 | 42 | 46 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 70 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/components/page/page.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | import '../recrec/recrec'; 5 | 6 | import componentCSS from './page.css?inline'; 7 | import recrecIcon from '../../images/recrec-logo.svg?raw'; 8 | 9 | /** 10 | * Page element. 11 | */ 12 | @customElement('recrec-page') 13 | export class RecRecPage extends LitElement { 14 | //==========================================================================|| 15 | // Class Properties || 16 | //==========================================================================|| 17 | 18 | //==========================================================================|| 19 | // Lifecycle Methods || 20 | //==========================================================================|| 21 | constructor() { 22 | super(); 23 | } 24 | 25 | /** 26 | * This method is called when the DOM is added for the first time 27 | */ 28 | firstUpdated() {} 29 | 30 | /** 31 | * This method is called before new DOM is updated and rendered 32 | * @param changedProperties Property that has been changed 33 | */ 34 | willUpdate(changedProperties: PropertyValues) {} 35 | 36 | //==========================================================================|| 37 | // Custom Methods || 38 | //==========================================================================|| 39 | async initData() {} 40 | 41 | //==========================================================================|| 42 | // Event Handlers || 43 | //==========================================================================|| 44 | 45 | //==========================================================================|| 46 | // Private Helpers || 47 | //==========================================================================|| 48 | 49 | //==========================================================================|| 50 | // Templates and Styles || 51 | //==========================================================================|| 52 | render() { 53 | return html` 54 | 77 | `; 78 | } 79 | 80 | static styles = [ 81 | css` 82 | ${unsafeCSS(componentCSS)} 83 | ` 84 | ]; 85 | } 86 | 87 | declare global { 88 | interface HTMLElementTagNameMap { 89 | 'recrec-page': RecRecPage; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/author-list/author-list.css: -------------------------------------------------------------------------------- 1 | .author-list { 2 | width: 100%; 3 | height: 100%; 4 | min-height: 0px; 5 | padding: 0px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: flex-start; 10 | align-items: flex-start; 11 | 12 | box-sizing: border-box; 13 | } 14 | 15 | .author-table { 16 | width: 100%; 17 | border-collapse: collapse; 18 | white-space: nowrap; 19 | box-sizing: border-box; 20 | --cell-padding-right: 50px; 21 | 22 | &:has(.author-row) { 23 | margin-top: 10px; 24 | } 25 | 26 | .col-icon { 27 | width: 0px; 28 | min-width: min-content; 29 | } 30 | 31 | .col-name { 32 | width: 0px; 33 | min-width: min-content; 34 | } 35 | 36 | .col-citation-count { 37 | width: 0px; 38 | min-width: min-content; 39 | } 40 | 41 | .col-paper-count { 42 | width: 0px; 43 | min-width: min-content; 44 | } 45 | 46 | .col-paper { 47 | width: 0px; 48 | min-width: min-content; 49 | } 50 | 51 | .author-row { 52 | cursor: pointer; 53 | box-sizing: border-box; 54 | 55 | .svg-icon { 56 | color: var(--gray-300); 57 | } 58 | 59 | &:hover { 60 | background-color: var(--gray-50); 61 | } 62 | 63 | &:active { 64 | background-color: var(--gray-100); 65 | } 66 | } 67 | 68 | & td { 69 | vertical-align: center; 70 | padding: 3px 0; 71 | height: 24px; 72 | box-sizing: border-box; 73 | } 74 | 75 | .icon { 76 | padding-left: calc(1rem - 2px); 77 | padding-right: 10px; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | width: min-content; 82 | position: relative; 83 | top: 2px; 84 | } 85 | 86 | .name { 87 | padding-right: var(--cell-padding-right); 88 | font-weight: 600; 89 | max-width: 300px; 90 | overflow: hidden; 91 | text-overflow: ellipsis; 92 | white-space: nowrap; 93 | vertical-align: top; 94 | } 95 | 96 | .paper-count { 97 | color: var(--gray-500); 98 | padding-right: var(--cell-padding-right); 99 | vertical-align: top; 100 | } 101 | 102 | .citation-count { 103 | color: var(--gray-500); 104 | vertical-align: top; 105 | padding-right: var(--cell-padding-right); 106 | } 107 | 108 | .paper { 109 | color: var(--gray-500); 110 | max-width: 500px; 111 | font-style: italic; 112 | overflow: hidden; 113 | text-overflow: ellipsis; 114 | white-space: nowrap; 115 | } 116 | } 117 | 118 | .svg-icon { 119 | display: inline-flex; 120 | justify-content: center; 121 | align-items: center; 122 | width: 1em; 123 | height: 1em; 124 | 125 | color: currentColor; 126 | transition: transform 80ms linear; 127 | transform-origin: center; 128 | 129 | & svg { 130 | fill: currentColor; 131 | width: 100%; 132 | height: 100%; 133 | } 134 | } 135 | 136 | @media only screen and (max-width: 768px) { 137 | .author-table { 138 | --cell-padding-right: 10px; 139 | 140 | & td { 141 | padding-top: 5px; 142 | padding-bottom: 5px; 143 | } 144 | 145 | .icon { 146 | display: none; 147 | } 148 | 149 | .name { 150 | padding-left: calc(1rem - 2px); 151 | max-width: unset; 152 | overflow: unset; 153 | text-overflow: unset; 154 | white-space: wrap; 155 | } 156 | 157 | .citation-count { 158 | white-space: wrap; 159 | } 160 | 161 | .paper-count { 162 | max-width: unset; 163 | white-space: wrap; 164 | } 165 | 166 | .paper { 167 | white-space: wrap; 168 | overflow: unset; 169 | text-overflow: unset; 170 | padding-right: calc(1rem - 2px); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/components/page/page.css: -------------------------------------------------------------------------------- 1 | .page { 2 | width: 100%; 3 | height: 100%; 4 | 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | 10 | box-sizing: border-box; 11 | 12 | /* background-image: radial-gradient( 13 | color-mix(in lab, var(--gray-700), transparent 50%) 0.5px, 14 | color-mix(in lab, white, transparent 50%) 0.5px 15 | ); 16 | background-size: 15px 15px; */ 17 | 18 | background-color: #ffffff; 19 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='450' height='450' viewBox='0 0 800 800'%3E%3Cg fill='none' stroke='%23EDEEE7' stroke-width='1'%3E%3Cpath d='M769 229L1037 260.9M927 880L731 737 520 660 309 538 40 599 295 764 126.5 879.5 40 599-197 493 102 382-31 229 126.5 79.5-69-63'/%3E%3Cpath d='M-31 229L237 261 390 382 603 493 308.5 537.5 101.5 381.5M370 905L295 764'/%3E%3Cpath d='M520 660L578 842 731 737 840 599 603 493 520 660 295 764 309 538 390 382 539 269 769 229 577.5 41.5 370 105 295 -36 126.5 79.5 237 261 102 382 40 599 -69 737 127 880'/%3E%3Cpath d='M520-140L578.5 42.5 731-63M603 493L539 269 237 261 370 105M902 382L539 269M390 382L102 382'/%3E%3Cpath d='M-222 42L126.5 79.5 370 105 539 269 577.5 41.5 927 80 769 229 902 382 603 493 731 737M295-36L577.5 41.5M578 842L295 764M40-201L127 80M102 382L-261 269'/%3E%3C/g%3E%3Cg fill='hsl%28232%2C%20100%25%2C%2094%25%29'%3E%3Ccircle cx='769' cy='229' r='5'/%3E%3Ccircle cx='539' cy='269' r='5'/%3E%3Ccircle cx='603' cy='493' r='5'/%3E%3Ccircle cx='731' cy='737' r='5'/%3E%3Ccircle cx='520' cy='660' r='5'/%3E%3Ccircle cx='309' cy='538' r='5'/%3E%3Ccircle cx='295' cy='764' r='5'/%3E%3Ccircle cx='40' cy='599' r='5'/%3E%3Ccircle cx='102' cy='382' r='5'/%3E%3Ccircle cx='127' cy='80' r='5'/%3E%3Ccircle cx='370' cy='105' r='5'/%3E%3Ccircle cx='578' cy='42' r='5'/%3E%3Ccircle cx='237' cy='261' r='5'/%3E%3Ccircle cx='390' cy='382' r='5'/%3E%3C/g%3E%3C/svg%3E"); 20 | } 21 | 22 | .headline { 23 | width: 100%; 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | padding: 10px 60px 0 60px; 28 | box-sizing: border-box; 29 | cursor: default; 30 | 31 | .tag-line { 32 | font-size: var(--font-u7); 33 | padding-top: 20px; 34 | color: var(--blue-900); 35 | margin-left: -20px; 36 | font-weight: 500; 37 | } 38 | 39 | .head-group { 40 | display: flex; 41 | align-items: center; 42 | gap: var(--font-u7); 43 | } 44 | 45 | a { 46 | text-decoration: none; 47 | } 48 | 49 | a.button { 50 | padding-top: var(--font-d2); 51 | font-size: var(--font-u6); 52 | text-decoration: none; 53 | color: initial; 54 | font-weight: 500; 55 | color: var(--gray-600); 56 | white-space: nowrap; 57 | 58 | &:hover { 59 | color: var(--blue-800); 60 | } 61 | } 62 | } 63 | 64 | .app-container { 65 | flex: 1 1 auto; 66 | width: 100%; 67 | 68 | recrec-app { 69 | width: 100%; 70 | height: 100%; 71 | display: flex; 72 | justify-content: center; 73 | align-items: flex-start; 74 | padding-top: var(--font-u7); 75 | box-sizing: border-box; 76 | } 77 | } 78 | 79 | .svg-icon { 80 | display: inline-flex; 81 | justify-content: center; 82 | align-items: center; 83 | flex: 0 1 140px; 84 | 85 | color: currentColor; 86 | transition: transform 80ms linear; 87 | transform-origin: center; 88 | 89 | & svg { 90 | fill: currentColor; 91 | width: 100%; 92 | height: 100%; 93 | } 94 | } 95 | 96 | @media only screen and (max-width: 768px) { 97 | .tag-line { 98 | display: none; 99 | } 100 | 101 | .headline { 102 | padding: 5px 20px 0 20px; 103 | 104 | a.button { 105 | font-size: var(--font-u5); 106 | } 107 | } 108 | 109 | .svg-icon { 110 | flex: 0 1 110px; 111 | } 112 | 113 | .app-container { 114 | recrec-app { 115 | padding-top: 10px; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/types/common-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions 3 | */ 4 | 5 | export enum AcademicAward { 6 | AAAI_FELLOW = 'aaai-fellow', 7 | ACM_FELLOW = 'acm-fellow', 8 | AAAS_FELLOW = 'aaas-fellow', 9 | ACM_DISSERTATION = 'acm-dissertation', 10 | ACM_DISTINGUISHED = 'acm-distinguished-member', 11 | ACM_GORDON = 'acm-gordon-bell', 12 | ACM_GRACE = 'acm-grace-hopper', 13 | ACM_SENIOR_MEMBER = 'acm-senior-member', 14 | ACM_TURING = 'acm-turing', 15 | AMACAD_MEMBER = 'aaa&s-member', 16 | IEEE_FELLOW = 'ieee-fellow', 17 | NAS_MEMBER = 'nas-member' 18 | } 19 | 20 | export interface SemanticCitationAuthor { 21 | authorId: string; 22 | name: string; 23 | } 24 | 25 | export interface SemanticCitation { 26 | paperId: string; 27 | title: string; 28 | authors: SemanticCitationAuthor[]; 29 | } 30 | 31 | export interface SemanticPaperCitationDetail { 32 | paperId: string; 33 | citations: SemanticCitation[]; 34 | } 35 | 36 | export interface SemanticPaperSearchResponse { 37 | offset: number; 38 | next?: number; 39 | data: SemanticPaper[]; 40 | } 41 | 42 | export interface SemanticExternalIds { 43 | DBLP?: string; 44 | ArXiv?: string; 45 | DOI?: string; 46 | CorpusId: number; 47 | } 48 | 49 | export interface SemanticPublicationVenue { 50 | id: string; 51 | name: string; 52 | type: string; 53 | alternate_names: string[]; 54 | url: string; 55 | } 56 | 57 | export interface SemanticAuthor { 58 | authorId: string; 59 | name: string; 60 | } 61 | 62 | export interface SemanticPaper { 63 | paperId: string; 64 | externalIds: SemanticExternalIds; 65 | corpusId: number; 66 | publicationVenue: SemanticPublicationVenue; 67 | url: string; 68 | title: string; 69 | venue: string; 70 | year: number; 71 | citationCount: number; 72 | publicationDate: string; 73 | authors: SemanticAuthor[]; 74 | } 75 | 76 | export interface SemanticAuthorSearch { 77 | authorId: string; 78 | name: string; 79 | } 80 | 81 | export interface SemanticAuthorSearchResponse { 82 | total: number; 83 | offset: number; 84 | next: number; 85 | data: SemanticAuthorSearch[]; 86 | } 87 | 88 | export type SemanticAuthorDetail = SemanticAuthorDetailContent | null; 89 | 90 | export interface SemanticAuthorDetailContent { 91 | authorId: string; 92 | affiliations?: string[]; 93 | citationCount?: number; 94 | homepage?: string; 95 | name?: string; 96 | paperCount?: number; 97 | hIndex?: number; 98 | url?: string; 99 | papers?: SemanticPaper[]; 100 | } 101 | 102 | export enum Step { 103 | Author = 'author', 104 | Paper = 'paper', 105 | Recommender = 'recommender' 106 | } 107 | 108 | export interface SimpleEventMessage { 109 | message: string; 110 | } 111 | 112 | export type Mutable = { 113 | -readonly [Key in keyof Type]: Type[Key]; 114 | }; 115 | 116 | export interface Point { 117 | x: number; 118 | y: number; 119 | } 120 | 121 | export interface Rect { 122 | x: number; 123 | y: number; 124 | width: number; 125 | height: number; 126 | } 127 | 128 | export interface RectPoint { 129 | x0: number; 130 | y0: number; 131 | x1: number; 132 | y1: number; 133 | } 134 | 135 | export interface Padding { 136 | top: number; 137 | right: number; 138 | bottom: number; 139 | left: number; 140 | } 141 | 142 | export interface Size { 143 | width: number; 144 | height: number; 145 | } 146 | 147 | export interface PromptModel { 148 | task: string; 149 | prompt: string; 150 | variables: string[]; 151 | temperature: number; 152 | stopSequences?: string[]; 153 | } 154 | 155 | export type TextGenWorkerMessage = 156 | | { 157 | command: 'startTextGen'; 158 | payload: { 159 | requestID: string; 160 | apiKey: string; 161 | prompt: string; 162 | temperature: number; 163 | stopSequences?: string[]; 164 | detail?: string; 165 | }; 166 | } 167 | | { 168 | command: 'finishTextGen'; 169 | payload: { 170 | requestID: string; 171 | apiKey: string; 172 | result: string; 173 | prompt: string; 174 | detail: string; 175 | }; 176 | } 177 | | { 178 | command: 'error'; 179 | payload: { 180 | requestID: string; 181 | originalCommand: string; 182 | message: string; 183 | }; 184 | }; 185 | -------------------------------------------------------------------------------- /src/components/recrec/recrec.css: -------------------------------------------------------------------------------- 1 | .app { 2 | width: calc(100% - 20px); 3 | height: calc(100% - 60px); 4 | 5 | max-height: 1200px; 6 | max-width: 1200px; 7 | box-sizing: border-box; 8 | 9 | /* border: 1px solid var(--gray-300); */ 10 | 11 | overflow: hidden; 12 | border-radius: var(--border-radius); 13 | background: white; 14 | box-shadow: 15 | 0 0 2px hsla(0, 0%, 0%, 0.1), 16 | 0 0 10px hsla(0, 0%, 0%, 0.04), 17 | 0 0 15px hsla(0, 0%, 0%, 0.04), 18 | 0 0 30px hsla(0, 0%, 0%, 0.04); 19 | } 20 | 21 | button { 22 | all: unset; 23 | } 24 | 25 | .view-container { 26 | box-sizing: border-box; 27 | display: flex; 28 | flex-flow: column; 29 | width: 100%; 30 | height: 100%; 31 | } 32 | 33 | .info-bar { 34 | padding: var(--container-v-padding) var(--container-h-padding); 35 | display: flex; 36 | flex-flow: row; 37 | align-items: center; 38 | gap: 20px; 39 | border-bottom: 1px solid var(--gray-300); 40 | box-sizing: border-box; 41 | 42 | .info-block { 43 | height: 100%; 44 | box-sizing: border-box; 45 | 46 | &[no-show] { 47 | display: none; 48 | } 49 | } 50 | 51 | .info-button { 52 | border: 1px solid var(--gray-300); 53 | border-radius: 5px; 54 | padding: 5px 10px; 55 | box-sizing: border-box; 56 | cursor: pointer; 57 | 58 | &:hover { 59 | border: 1px solid var(--gray-400); 60 | } 61 | 62 | &[is-unset] { 63 | color: var(--gray-600); 64 | } 65 | } 66 | 67 | .menu-button { 68 | border: 1px solid var(--gray-300); 69 | border-radius: 5px; 70 | padding: 5px 10px; 71 | cursor: pointer; 72 | height: 100%; 73 | box-sizing: border-box; 74 | 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | 79 | &:hover { 80 | border: 1px solid var(--gray-400); 81 | } 82 | } 83 | } 84 | 85 | .step-content-container { 86 | display: flex; 87 | flex-flow: column; 88 | width: 100%; 89 | flex: 1; 90 | min-height: 0px; 91 | position: relative; 92 | 93 | .content-view { 94 | min-height: 0px; 95 | position: absolute; 96 | height: 100%; 97 | width: 100%; 98 | 99 | &[no-show] { 100 | display: none; 101 | } 102 | } 103 | } 104 | 105 | .svg-icon { 106 | display: inline-flex; 107 | justify-content: center; 108 | align-items: center; 109 | width: 1em; 110 | height: 1em; 111 | 112 | color: currentColor; 113 | transition: transform 80ms linear; 114 | transform-origin: center; 115 | 116 | & svg { 117 | fill: currentColor; 118 | width: 100%; 119 | height: 100%; 120 | } 121 | } 122 | 123 | .hamburger-icon { 124 | --hamburger-width: 16px; 125 | --hamburger-height: calc(sin(45deg) * var(--hamburger-width)); 126 | --hamburger-line-height: 2px; 127 | 128 | width: var(--hamburger-width); 129 | height: var(--hamburger-height); 130 | 131 | transform: rotate(0deg); 132 | transition: 300ms ease-in-out; 133 | } 134 | 135 | .hamburger-icon span { 136 | position: absolute; 137 | height: var(--hamburger-line-height); 138 | border-radius: var(--hamburger-line-height); 139 | width: 100%; 140 | 141 | background: currentColor; 142 | opacity: 1; 143 | left: 0; 144 | 145 | transform: rotate(0deg); 146 | transform-origin: left center; 147 | transition: 150ms ease-in-out; 148 | } 149 | 150 | .hamburger-icon span:nth-child(1) { 151 | top: 0px; 152 | } 153 | 154 | .hamburger-icon span:nth-child(2) { 155 | top: calc((var(--hamburger-height) / 2) - (var(--hamburger-line-height) / 2)); 156 | } 157 | 158 | .hamburger-icon span:nth-child(3) { 159 | top: calc(var(--hamburger-height) - var(--hamburger-line-height) / 1); 160 | } 161 | 162 | .hamburger-icon[open] span:nth-child(1) { 163 | transform: rotate(45deg); 164 | top: 0px; 165 | left: calc((var(--hamburger-width) - var(--hamburger-height)) / 2); 166 | } 167 | 168 | .hamburger-icon[open] span:nth-child(2) { 169 | width: 0%; 170 | opacity: 0; 171 | } 172 | 173 | .hamburger-icon[open] span:nth-child(3) { 174 | transform: rotate(-45deg); 175 | top: var(--hamburger-height); 176 | left: calc((var(--hamburger-width) - var(--hamburger-height)) / 2); 177 | } 178 | 179 | @media only screen and (max-width: 768px) { 180 | .app { 181 | width: calc(100% - 0px); 182 | height: calc(100% - 0px); 183 | box-shadow: none; 184 | border-radius: 0px; 185 | } 186 | 187 | .info-bar { 188 | gap: 10px; 189 | } 190 | 191 | .info-button { 192 | max-width: 140px; 193 | text-overflow: ellipsis; 194 | overflow: hidden; 195 | white-space: nowrap; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/components/header-bar/header-bar.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | import { Step } from '../../types/common-types'; 5 | import type { SemanticAuthorDetail } from '../../types/common-types'; 6 | 7 | import iconCaret from '../../images/icon-caret-down.svg?raw'; 8 | import componentCSS from './header-bar.css?inline'; 9 | 10 | const steps = [Step.Author, Step.Paper, Step.Recommender]; 11 | 12 | const titleString: Record = { 13 | [Step.Author]: 'Find Your Semantic Scholar Profile', 14 | [Step.Paper]: 'Select Representative Papers', 15 | [Step.Recommender]: 'Refine the Potential Recommenders' 16 | }; 17 | 18 | /** 19 | * Header bar element. 20 | */ 21 | @customElement('recrec-header-bar') 22 | export class RecRecHeaderBar extends LitElement { 23 | //==========================================================================|| 24 | // Class Properties || 25 | //==========================================================================|| 26 | @property({ attribute: false }) 27 | curStep = Step.Author; 28 | 29 | @property({ attribute: false }) 30 | selectedProfile: SemanticAuthorDetail | null = null; 31 | 32 | get curStepIndex() { 33 | return steps.indexOf(this.curStep); 34 | } 35 | 36 | //==========================================================================|| 37 | // Lifecycle Methods || 38 | //==========================================================================|| 39 | constructor() { 40 | super(); 41 | } 42 | 43 | /** 44 | * This method is called when the DOM is added for the first time 45 | */ 46 | firstUpdated() {} 47 | 48 | /** 49 | * This method is called before new DOM is updated and rendered 50 | * @param changedProperties Property that has been changed 51 | */ 52 | willUpdate(changedProperties: PropertyValues) {} 53 | 54 | //==========================================================================|| 55 | // Custom Methods || 56 | //==========================================================================|| 57 | async initData() {} 58 | 59 | notifyParentMoveStep(direction: 'pre' | 'next') { 60 | const event = new CustomEvent<'pre' | 'next'>('step-clicked', { 61 | bubbles: true, 62 | composed: true, 63 | detail: direction 64 | }); 65 | this.dispatchEvent(event); 66 | } 67 | 68 | //==========================================================================|| 69 | // Event Handlers || 70 | //==========================================================================|| 71 | 72 | //==========================================================================|| 73 | // Private Helpers || 74 | //==========================================================================|| 75 | 76 | //==========================================================================|| 77 | // Templates and Styles || 78 | //==========================================================================|| 79 | render() { 80 | return html` 81 |
82 | 91 | 92 |
93 | Step ${this.curStepIndex + 1}/${steps.length} 96 | ${titleString[this.curStep]} 97 |
98 | 99 | 109 |
110 | `; 111 | } 112 | 113 | static styles = [ 114 | css` 115 | ${unsafeCSS(componentCSS)} 116 | ` 117 | ]; 118 | } 119 | 120 | declare global { 121 | interface HTMLElementTagNameMap { 122 | 'recrec-header-bar': RecRecHeaderBar; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

RecRec 2 | 3 | 4 | RecRec logo. 5 |

6 | 7 | [![Github Actions Status](https://github.com/xiaohk/recrec/workflows/build/badge.svg)](https://github.com/xiaohk/recrec/actions/workflows/build.yml) 8 | [![license](https://img.shields.io/badge/License-MIT-blue)](https://github.com/xiaohk/recrec/blob/main/LICENSE) 9 | [![npm](https://img.shields.io/npm/v/recrec?color=orange)](https://www.npmjs.com/package/recrec) 10 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.12697177.svg)](https://doi.org/10.5281/zenodo.12697177) 11 | 12 | Recommender for recommendation letter writers 👍 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
🚀 RecRec Demo📺 Demo Video
24 | 25 | ## What is RecRec? 26 | 27 | RecRec is a practical tool for finding academic recommendation letter writers. 28 | Whether you are preparing for your tenure package, job application, or green card petition, RecRec helps you quickly identify the best letter writers. 29 | RecRec highlights connections between you and potential recommenders and lets you filter by citations, awards, and other criteria. 30 | Save time and get the most impactful recommendations with RecRec! 31 | 32 | ### Demo Video 33 | 34 |
35 | Click to see the high-resolution demo video! 36 | 37 |
38 | 39 | ## How Does RecRec Work? 40 | 41 | RecRec uses [Semantic Scholar's](https://www.semanticscholar.org) citation database to analyze and identify all researchers who have cited your papers. 42 | In addition, it uses the [Academic Award database](https://github.com/xiaohk/academic-awards) to highlight researchers with awards, such as ACM Fellow and IEEE Fellow. 43 | Finally, RecRec provides an easy-to-use interface to help you quickly sort, filter, and select potential recommenders. 44 | 45 | ## Get Started 46 | 47 | To use RecRec, visit: . 48 | 49 | To find potential recommendation letter writers, follow these three steps: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
Step 1Type your name to identify your semantic scholar profile
Step 2Select your most representative papers
Step 3Browse and filter potential letter writers
OptionalRepeat Steps 2 and 3 if needed
74 | 75 | ## Developing RecRec 76 | 77 | Clone or download this repository: 78 | 79 | ```bash 80 | git clone git@github.com:xiaohk/recrec.git 81 | ``` 82 | 83 | Install the dependencies: 84 | 85 | ```bash 86 | npm install 87 | ``` 88 | 89 | Then run RecRec: 90 | 91 | ``` 92 | npm run dev 93 | ``` 94 | 95 | Navigate to localhost:3000. You should see RecRec running in your browser :) 96 | 97 | ## Credits 98 | 99 | RecRec is created by Jay Wang. 100 | 101 | We appreciate valuable feedback from [Polo Chau](https://poloclub.github.io/polochau/), [Kaan Sancak](https://www.kaansancak.com), [Haekyu Park](https://haekyu.com), and [Alex Kale](https://people.cs.uchicago.edu/~kalea/)! 🙌 102 | 103 | ## Citation 104 | 105 | If you find RecRec useful, please consider citing it. 106 | 107 | ```bibtex 108 | @misc{wangRecRecRecommenderRecommender2024, 109 | title = {{{RecRec}}: {{Recommender}} for {{Recommender Letter Writers}}}, 110 | shorttitle = {{{RecRec}}}, 111 | author = {Wang, Zijie J.}, 112 | year = {2024}, 113 | doi = {10.5281/ZENODO.12697177}, 114 | url = {https://zenodo.org/doi/10.5281/zenodo.12697177}, 115 | urldate = {2024-07-09}, 116 | copyright = {MIT License} 117 | } 118 | ``` 119 | 120 | ## License 121 | 122 | The software is available under the [MIT License](https://github.com/xiaohk/recrec/blob/main/LICENSE). 123 | 124 | ## Contribution 125 | 126 | Feature requests, bug reports, and fixes are all welcome! Start by [opening an issue](https://github.com/xiaohk/recrec/issues/new). 127 | 128 | ## Contact 129 | 130 | If you have any questions, feel free to [open an issue](https://github.com/xiaohk/recrec/issues/new) or contact [Jay Wang](https://zijie.wang). 131 | -------------------------------------------------------------------------------- /src/recrec-api/wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "recrec-api" 3 | main = "src/index.ts" 4 | compatibility_date = "2024-07-01" 5 | 6 | # Automatically place your workloads in an optimal location to minimize latency. 7 | # If you are running back-end logic in a Worker, running it closer to your back-end infrastructure 8 | # rather than the end user may result in better performance. 9 | # Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement 10 | # [placement] 11 | # mode = "smart" 12 | 13 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) 14 | # Docs: 15 | # - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 16 | # Note: Use secrets to store sensitive data. 17 | # - https://developers.cloudflare.com/workers/configuration/secrets/ 18 | # [vars] 19 | # MY_VARIABLE = "production_value" 20 | 21 | # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network 22 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai 23 | # [ai] 24 | # binding = "AI" 25 | 26 | # Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function. 27 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets 28 | # [[analytics_engine_datasets]] 29 | # binding = "MY_DATASET" 30 | 31 | # Bind a headless browser instance running on Cloudflare's global network. 32 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering 33 | # [browser] 34 | # binding = "MY_BROWSER" 35 | 36 | # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. 37 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases 38 | # [[d1_databases]] 39 | # binding = "MY_DB" 40 | # database_name = "my-database" 41 | # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 42 | 43 | # Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers. 44 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms 45 | # [[dispatch_namespaces]] 46 | # binding = "MY_DISPATCHER" 47 | # namespace = "my-namespace" 48 | 49 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. 50 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. 51 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects 52 | # [[durable_objects.bindings]] 53 | # name = "MY_DURABLE_OBJECT" 54 | # class_name = "MyDurableObject" 55 | 56 | # Durable Object migrations. 57 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations 58 | # [[migrations]] 59 | # tag = "v1" 60 | # new_classes = ["MyDurableObject"] 61 | 62 | # Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers. 63 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive 64 | # [[hyperdrive]] 65 | # binding = "MY_HYPERDRIVE" 66 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 67 | 68 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. 69 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces 70 | # [[kv_namespaces]] 71 | # binding = "MY_KV_NAMESPACE" 72 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 73 | 74 | # Bind an mTLS certificate. Use to present a client certificate when communicating with another service. 75 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates 76 | # [[mtls_certificates]] 77 | # binding = "MY_CERTIFICATE" 78 | # certificate_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 79 | 80 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. 81 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues 82 | # [[queues.producers]] 83 | # binding = "MY_QUEUE" 84 | # queue = "my-queue" 85 | 86 | # Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. 87 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues 88 | # [[queues.consumers]] 89 | # queue = "my-queue" 90 | 91 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 92 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets 93 | # [[r2_buckets]] 94 | # binding = "MY_BUCKET" 95 | # bucket_name = "my-bucket" 96 | 97 | # Bind another Worker service. Use this binding to call another Worker without network overhead. 98 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 99 | # [[services]] 100 | # binding = "MY_SERVICE" 101 | # service = "my-service" 102 | 103 | # Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases. 104 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes 105 | # [[vectorize]] 106 | # binding = "MY_INDEX" 107 | # index_name = "my-index" 108 | -------------------------------------------------------------------------------- /src/images/recrec-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/paper-view/paper-view.css: -------------------------------------------------------------------------------- 1 | .paper-view { 2 | width: 100%; 3 | height: 100%; 4 | padding: 0; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: flex-start; 10 | 11 | box-sizing: border-box; 12 | position: relative; 13 | } 14 | 15 | .table-container { 16 | width: 100%; 17 | height: 100%; 18 | overflow-y: auto; 19 | 20 | border-bottom: 1px solid var(--gray-300); 21 | border-left: none; 22 | border-right: none; 23 | } 24 | 25 | .paper-table { 26 | --table-v-padding: 11px; 27 | --table-h-padding: 30px; 28 | 29 | width: 100%; 30 | border-collapse: separate; 31 | border-spacing: 0; 32 | empty-cells: show; 33 | line-height: 1.24; 34 | margin-bottom: 10px; 35 | box-sizing: border-box; 36 | 37 | & thead { 38 | position: sticky; 39 | top: 0; 40 | background: white; 41 | z-index: 2; 42 | } 43 | 44 | .selected-cell { 45 | position: relative; 46 | vertical-align: top; 47 | text-align: center; 48 | padding: var(--table-v-padding) var(--table-h-padding) 0 49 | var(--container-h-padding); 50 | 51 | & input[type='checkbox'] { 52 | position: relative; 53 | margin: 0; 54 | z-index: 1; 55 | } 56 | 57 | & label { 58 | position: absolute; 59 | width: 36px; 60 | height: 36px; 61 | transform: translateX(-60%); 62 | } 63 | } 64 | 65 | .title-cell { 66 | vertical-align: top; 67 | text-align: left; 68 | padding: var(--table-v-padding) var(--table-h-padding) 0 0; 69 | 70 | display: flex; 71 | flex-flow: column nowrap; 72 | 73 | .title { 74 | font-weight: 500; 75 | } 76 | 77 | .author { 78 | font-size: var(--font-d3); 79 | 80 | max-width: 600px; 81 | white-space: nowrap; 82 | overflow: hidden; 83 | text-overflow: ellipsis; 84 | } 85 | 86 | .venue { 87 | font-size: var(--font-d3); 88 | 89 | max-width: 600px; 90 | white-space: nowrap; 91 | overflow: hidden; 92 | text-overflow: ellipsis; 93 | } 94 | } 95 | 96 | .citation-cell { 97 | vertical-align: top; 98 | text-align: right; 99 | padding: var(--table-v-padding) var(--table-h-padding) 0 0; 100 | width: 30px; 101 | font-size: var(--font-d2); 102 | } 103 | 104 | .date-cell { 105 | vertical-align: top; 106 | text-align: right; 107 | padding: var(--table-v-padding) var(--container-h-padding) 0 0; 108 | font-size: var(--font-d2); 109 | } 110 | 111 | .header-row { 112 | & th { 113 | white-space: nowrap; 114 | border-bottom: 1px solid var(--gray-300); 115 | 116 | &.header-cell { 117 | padding-top: 12px; 118 | padding-bottom: 12px; 119 | font-size: 1rem; 120 | 121 | &:first-child { 122 | padding-left: var(--container-h-padding); 123 | } 124 | 125 | &:last-child { 126 | padding-right: var(--container-h-padding); 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | button { 134 | all: unset; 135 | } 136 | 137 | button.header-button { 138 | color: var(--indigo-600); 139 | cursor: pointer; 140 | width: min-content; 141 | font-weight: 500; 142 | text-transform: uppercase; 143 | 144 | &:hover { 145 | text-decoration: underline; 146 | } 147 | 148 | &:active { 149 | color: var(--indigo-900); 150 | } 151 | } 152 | 153 | .footer { 154 | box-sizing: border-box; 155 | padding: var(--container-v-padding) var(--container-h-padding); 156 | width: 100%; 157 | display: flex; 158 | flex-flow: row; 159 | justify-content: flex-end; 160 | } 161 | 162 | button { 163 | all: unset; 164 | } 165 | 166 | .confirm-button { 167 | border-radius: 5px; 168 | border: 1px solid var(--blue-600); 169 | background: var(--blue-600); 170 | color: white; 171 | font-weight: 500; 172 | 173 | padding: 8px 16px; 174 | box-sizing: border-box; 175 | position: relative; 176 | cursor: pointer; 177 | 178 | transition: 179 | background linear 100ms, 180 | border linear 100ms; 181 | 182 | &:disabled { 183 | cursor: no-drop; 184 | border: 1px solid var(--gray-300); 185 | color: var(--gray-600); 186 | background: var(--gray-100); 187 | } 188 | 189 | &:not(:disabled) { 190 | &:hover { 191 | border: 1px solid color-mix(in lab, var(--blue-600), white 10%); 192 | background-color: color-mix(in lab, var(--blue-600), white 10%); 193 | } 194 | 195 | &:active { 196 | background: var(--blue-600); 197 | border: 1px solid var(--blue-600); 198 | } 199 | } 200 | } 201 | 202 | .progress-overlay { 203 | width: 100%; 204 | height: 100%; 205 | position: absolute; 206 | z-index: 5; 207 | background: white; 208 | border-radius: 10px; 209 | 210 | display: flex; 211 | flex-flow: column; 212 | align-items: center; 213 | justify-content: center; 214 | gap: 10px; 215 | transition: opacity 300ms ease-in-out; 216 | 217 | --track-width: 5px; 218 | --sl-color-primary-600: var(--blue-700); 219 | 220 | &[is-completed] { 221 | opacity: 0; 222 | pointer-events: none; 223 | } 224 | 225 | .loader { 226 | --stroke: 5px; 227 | 228 | width: 128px; 229 | aspect-ratio: 1; 230 | border-radius: 50%; 231 | 232 | background: 233 | radial-gradient(farthest-side, var(--blue-700) 94%, #0000) 234 | top/var(--stroke) var(--stroke) no-repeat, 235 | conic-gradient(#0000 30%, var(--blue-700)); 236 | 237 | -webkit-mask: radial-gradient( 238 | farthest-side, 239 | #0000 calc(100% - var(--stroke)), 240 | #000 0 241 | ); 242 | animation: rotation 1200ms infinite linear; 243 | } 244 | } 245 | 246 | @keyframes rotation { 247 | 100% { 248 | transform: rotate(1turn); 249 | } 250 | } 251 | 252 | @media only screen and (max-width: 768px) { 253 | .paper-table { 254 | table-layout: fixed; 255 | --table-h-padding: 0px; 256 | 257 | .col-checkbox { 258 | width: 36px; 259 | } 260 | 261 | .col-citation { 262 | width: 42px; 263 | } 264 | 265 | .col-date { 266 | width: 64px; 267 | } 268 | 269 | .title-cell { 270 | padding: var(--table-v-padding) 0px 0 12px; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/components/author-list/author-list.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | import { searchAuthorDetails } from '../../api/semantic-scholar'; 5 | 6 | import type { 7 | SemanticAuthorSearch, 8 | SemanticAuthorDetail 9 | } from '../../types/common-types'; 10 | 11 | import iconPerson from '../../images/icon-person.svg?raw'; 12 | import componentCSS from './author-list.css?inline'; 13 | 14 | const AUTHORS_PER_PAGE = 50; 15 | let authorDetailAPITimeout: number | null = null; 16 | 17 | /** 18 | * Author list element. 19 | */ 20 | @customElement('recrec-author-list') 21 | export class RecRecAuthorList extends LitElement { 22 | //==========================================================================|| 23 | // Class Properties || 24 | //==========================================================================|| 25 | @property({ attribute: false }) 26 | authors: SemanticAuthorSearch[] = []; 27 | 28 | @state() 29 | authorDetails: SemanticAuthorDetail[] = []; 30 | 31 | authorStartIndex = 0; 32 | 33 | //==========================================================================|| 34 | // Lifecycle Methods || 35 | //==========================================================================|| 36 | constructor() { 37 | super(); 38 | } 39 | 40 | /** 41 | * This method is called when the DOM is added for the first time 42 | */ 43 | firstUpdated() {} 44 | 45 | /** 46 | * This method is called before new DOM is updated and rendered 47 | * @param changedProperties Property that has been changed 48 | */ 49 | willUpdate(changedProperties: PropertyValues) { 50 | if (changedProperties.has('authors')) { 51 | // If the search authors have changed, we need to fetch the new details 52 | this.authorStartIndex = 0; 53 | this.updateAuthorDetails().then( 54 | () => {}, 55 | () => {} 56 | ); 57 | } 58 | } 59 | 60 | //==========================================================================|| 61 | // Custom Methods || 62 | //==========================================================================|| 63 | async initData() {} 64 | 65 | //==========================================================================|| 66 | // Event Handlers || 67 | //==========================================================================|| 68 | authorRowClicked(author: SemanticAuthorDetail) { 69 | // Notify the parent about the selection 70 | const event: CustomEvent = new CustomEvent( 71 | 'author-row-clicked', 72 | { 73 | bubbles: true, 74 | composed: true, 75 | detail: author 76 | } 77 | ); 78 | this.dispatchEvent(event); 79 | } 80 | 81 | //==========================================================================|| 82 | // Private Helpers || 83 | //==========================================================================|| 84 | async updateAuthorDetails(retry = 3) { 85 | if (this.authors.length === 0) { 86 | this.authorDetails = []; 87 | return; 88 | } 89 | 90 | this.updateIsSearching(true); 91 | 92 | if (authorDetailAPITimeout !== null) { 93 | clearTimeout(authorDetailAPITimeout); 94 | } 95 | 96 | try { 97 | const fields = 98 | 'authorId,name,affiliations,paperCount,citationCount,papers.title'; 99 | const data = await searchAuthorDetails( 100 | this.authors.map(d => d.authorId), 101 | fields 102 | ); 103 | this.authorDetails = data; 104 | this.updateIsSearching(false); 105 | } catch (e) { 106 | // Try again after some delay 107 | console.error( 108 | `Author detail API failed with error (retry remain: ${retry}): ${e}` 109 | ); 110 | 111 | if (retry > 0) { 112 | authorDetailAPITimeout = setTimeout(() => { 113 | this.updateAuthorDetails(retry - 1).then( 114 | () => {}, 115 | () => {} 116 | ); 117 | }, 1000); 118 | } else { 119 | this.updateIsSearching(false); 120 | } 121 | } 122 | } 123 | 124 | updateIsSearching(isSearching: boolean) { 125 | const event = new CustomEvent('is-searching-changed', { 126 | bubbles: true, 127 | composed: true, 128 | detail: isSearching 129 | }); 130 | this.dispatchEvent(event); 131 | } 132 | 133 | //==========================================================================|| 134 | // Templates and Styles || 135 | //==========================================================================|| 136 | render() { 137 | // Collect the authors 138 | let authors = html``; 139 | 140 | for (const author of this.authorDetails.slice( 141 | this.authorStartIndex, 142 | this.authorStartIndex + AUTHORS_PER_PAGE 143 | )) { 144 | if (author === null) continue; 145 | 146 | let latestPaper = ''; 147 | if (author.papers !== undefined && author.papers.length > 0) { 148 | latestPaper = author.papers[0].title; 149 | } 150 | 151 | authors = html`${authors} 152 | { 155 | this.authorRowClicked(author); 156 | }} 157 | > 158 | 159 |
${unsafeHTML(iconPerson)}
160 | 161 | 162 | ${author.name}${author.affiliations!.length > 0 163 | ? ` (${author.affiliations![0]})` 164 | : ''} 165 | 166 | ${author.citationCount} citations 167 | ${author.paperCount} papers 168 | "${latestPaper}" 169 | `; 170 | } 171 | 172 | return html` 173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | ${authors} 183 |
184 |
185 | `; 186 | } 187 | 188 | static styles = [ 189 | css` 190 | ${unsafeCSS(componentCSS)} 191 | ` 192 | ]; 193 | } 194 | 195 | declare global { 196 | interface HTMLElementTagNameMap { 197 | 'recrec-author-list': RecRecAuthorList; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/components/slider/slider.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { styleMap } from 'lit/directives/style-map.js'; 4 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 5 | 6 | import componentCSS from './slider.css?inline'; 7 | 8 | export interface SliderStyleConfig { 9 | foregroundColor: string; 10 | backgroundColor: string; 11 | thumbColor: string; 12 | alignInner: boolean; 13 | } 14 | 15 | /** 16 | * Slider element. 17 | * 18 | */ 19 | @customElement('nightjar-slider') 20 | export class NightjarSlider extends LitElement { 21 | // ===== Class properties ====== 22 | @property({ type: Number }) 23 | min = 0; 24 | 25 | @property({ type: Number }) 26 | max = 1; 27 | 28 | @property({ type: Number }) 29 | curValue: number | null = null; 30 | 31 | @property({ attribute: false }) 32 | styleConfig: SliderStyleConfig | null = null; 33 | 34 | @query('.background-track') 35 | backgroundTrackElement: HTMLElement | undefined; 36 | 37 | @query('.range-track') 38 | rangeTrackElement: HTMLElement | undefined; 39 | 40 | @query('#slider-middle-thumb') 41 | middleThumbElement: HTMLElement | undefined; 42 | 43 | // ===== Lifecycle Methods ====== 44 | constructor() { 45 | super(); 46 | } 47 | 48 | firstUpdated() { 49 | if (this.curValue === null) { 50 | this.curValue = this.min; 51 | } 52 | this.syncThumb(); 53 | } 54 | 55 | /** 56 | * This method is called before new DOM is updated and rendered 57 | * @param changedProperties Property that has been changed 58 | */ 59 | willUpdate(changedProperties: PropertyValues) { 60 | if (changedProperties.has('curValue') && this.curValue !== null) { 61 | this.syncThumb(); 62 | } else if (changedProperties.has('min')) { 63 | this.syncThumb(); 64 | } else if (changedProperties.has('max')) { 65 | this.syncThumb(); 66 | } 67 | } 68 | 69 | // ===== Custom Methods ====== 70 | initData = async () => {}; 71 | 72 | /** 73 | * Sync thumb position to this.curValue 74 | */ 75 | syncThumb() { 76 | if (this.curValue !== null) { 77 | // Update the thumb to reflect the cur value 78 | const track = this.backgroundTrackElement; 79 | const thumb = this.middleThumbElement; 80 | const rangeTrack = this.rangeTrackElement; 81 | 82 | if (track && thumb && rangeTrack) { 83 | const thumbBBox = thumb.getBoundingClientRect(); 84 | const trackBBox = track.getBoundingClientRect(); 85 | 86 | // Update the thumb and the range track 87 | const progress = (this.curValue - this.min) / (this.max - this.min); 88 | const xPos = progress * trackBBox.width - thumbBBox.width / 2; 89 | thumb.style.left = `${xPos}px`; 90 | rangeTrack.style.width = `${Math.max(0, xPos)}px`; 91 | } 92 | } 93 | } 94 | 95 | // ===== Event Methods ====== 96 | /** 97 | * Event handler for the thumb mousedown 98 | */ 99 | thumbMouseDownHandler(e: MouseEvent | TouchEvent) { 100 | e.preventDefault(); 101 | e.stopPropagation(); 102 | 103 | const thumb = e.target! as HTMLElement; 104 | if (!thumb.id.includes('thumb')) { 105 | console.error('Thumb event target is not thumb itself.'); 106 | } 107 | 108 | // const eventBlocker = this.component.querySelector('.grab-blocker')!; 109 | const track = thumb.parentElement!; 110 | const rangeTrack = track.querySelector('.range-track') as HTMLElement; 111 | 112 | const thumbBBox = thumb.getBoundingClientRect(); 113 | const trackBBox = track.getBoundingClientRect(); 114 | 115 | const trackWidth = trackBBox.width; 116 | // Need to use focus instead of active because FireFox doesn't treat dragging 117 | // as active 118 | thumb.focus(); 119 | 120 | const mouseMoveHandler = (e: MouseEvent | TouchEvent) => { 121 | e.preventDefault(); 122 | e.stopPropagation(); 123 | 124 | // Block the mouse event outside the slider 125 | // eventBlocker.classList.add('activated'); 126 | 127 | let pageX = 0; 128 | if ((e as MouseEvent).pageX) { 129 | pageX = (e as MouseEvent).pageX; 130 | } else { 131 | pageX = (e as TouchEvent).touches[0].pageX; 132 | } 133 | 134 | const deltaX = pageX - trackBBox.x; 135 | const progress = Math.min(1, Math.max(0, deltaX / trackWidth)); 136 | 137 | // Move the thumb 138 | thumb.setAttribute('data-curValue', String(progress)); 139 | 140 | // Compute the position to move the thumb to 141 | const xPos = progress * trackBBox.width - thumbBBox.width / 2; 142 | thumb.style.left = `${xPos}px`; 143 | rangeTrack.style.width = `${Math.max(0, xPos)}px`; 144 | 145 | // Notify the parent about the change 146 | const curValue = this.min + (this.max - this.min) * progress; 147 | const event = new CustomEvent('valueChanged', { 148 | detail: curValue 149 | }); 150 | this.dispatchEvent(event); 151 | 152 | // Update the tooltip thumb 153 | // this.moveTimeSliderThumb(progress); 154 | }; 155 | 156 | const mouseUpHandler = () => { 157 | document.removeEventListener('mousemove', mouseMoveHandler); 158 | document.removeEventListener('mouseup', mouseUpHandler); 159 | 160 | // Also handle touch screen 161 | document.removeEventListener('touchmove', mouseMoveHandler); 162 | document.removeEventListener('touchend', mouseUpHandler); 163 | 164 | // eventBlocker.classList.remove('activated'); 165 | thumb.blur(); 166 | }; 167 | 168 | // Listen to mouse move on the whole page (users can drag outside of the 169 | // thumb, track, or even the tool!) 170 | document.addEventListener('mousemove', mouseMoveHandler); 171 | document.addEventListener('mouseup', mouseUpHandler); 172 | 173 | // Also handle touch screen 174 | document.addEventListener('touchmove', mouseMoveHandler); 175 | document.addEventListener('touchend', mouseUpHandler); 176 | } 177 | 178 | // ===== Templates and Styles ====== 179 | render() { 180 | const cssVariables = { 181 | '--foreground-color': 182 | this.styleConfig?.foregroundColor || 'hsl(174, 100%, 29.41%)', 183 | '--background-color': 184 | this.styleConfig?.backgroundColor || 'hsl(174, 41.28%, 78.63%)', 185 | '--thumb-color': this.styleConfig?.thumbColor || 'hsl(174, 100%, 29.41%)' 186 | }; 187 | 188 | return html` 189 |
194 |
195 |
196 |
this.thumbMouseDownHandler(e)} 201 | @touchstart=${(e: TouchEvent) => this.thumbMouseDownHandler(e)} 202 | > 203 |
204 | 205 |
206 |
207 |
208 | `; 209 | } 210 | 211 | static styles = [ 212 | css` 213 | ${unsafeCSS(componentCSS)} 214 | ` 215 | ]; 216 | } 217 | 218 | declare global { 219 | interface HTMLElementTagNameMap { 220 | 'nightjar-slider': NightjarSlider; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/api/semantic-scholar.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SemanticAuthorSearchResponse, 3 | SemanticAuthorSearch, 4 | SemanticAuthorDetail, 5 | SemanticPaperSearchResponse, 6 | SemanticPaper, 7 | SemanticPaperCitationDetail 8 | } from '../types/common-types'; 9 | import { downloadJSON } from '@xiaohk/utils'; 10 | 11 | import paperSearchMockJSON from '../../test/api-responses/paper-search.json'; 12 | import citationSearchMockJSON from '../../test/api-responses/citation-search.json'; 13 | import authorCitationSearchMockJSON0 from '../../test/api-responses/author-citation-search-9-0.json'; 14 | import authorCitationSearchMockJSON1 from '../../test/api-responses/author-citation-search-9-1.json'; 15 | import authorCitationSearchMockJSON2 from '../../test/api-responses/author-citation-search-9-2.json'; 16 | import authorCitationSearchMockJSON3 from '../../test/api-responses/author-citation-search-9-3.json'; 17 | import authorCitationSearchMockJSON4 from '../../test/api-responses/author-citation-search-9-4.json'; 18 | import authorCitationSearchMockJSON5 from '../../test/api-responses/author-citation-search-9-5.json'; 19 | import authorCitationSearchMockJSON6 from '../../test/api-responses/author-citation-search-9-6.json'; 20 | import authorCitationSearchMockJSON7 from '../../test/api-responses/author-citation-search-9-7.json'; 21 | import authorCitationSearchMockJSON8 from '../../test/api-responses/author-citation-search-9-8.json'; 22 | import authorCitationSearchMockJSON9 from '../../test/api-responses/author-citation-search-9-9.json'; 23 | 24 | const MOCK_HTTP_CALL = false; 25 | 26 | const paperSearchMock = paperSearchMockJSON as SemanticPaperSearchResponse; 27 | const citationSearchMock = 28 | citationSearchMockJSON as SemanticPaperCitationDetail[]; 29 | 30 | let authorCitationMockCounter = 0; 31 | const authorCitationSearchMocks = [ 32 | authorCitationSearchMockJSON0 as SemanticAuthorDetail[], 33 | authorCitationSearchMockJSON1 as SemanticAuthorDetail[], 34 | authorCitationSearchMockJSON2 as SemanticAuthorDetail[], 35 | authorCitationSearchMockJSON3 as SemanticAuthorDetail[], 36 | authorCitationSearchMockJSON4 as SemanticAuthorDetail[], 37 | authorCitationSearchMockJSON5 as SemanticAuthorDetail[], 38 | authorCitationSearchMockJSON6 as SemanticAuthorDetail[], 39 | authorCitationSearchMockJSON7 as SemanticAuthorDetail[], 40 | authorCitationSearchMockJSON8 as SemanticAuthorDetail[], 41 | authorCitationSearchMockJSON9 as SemanticAuthorDetail[] 42 | ]; 43 | 44 | const proxyFetch = (url: string, options?: RequestInit) => { 45 | const proxyUrl = 'https://recrec-api.jay-8dc.workers.dev'; 46 | const newURL = `${proxyUrl}?proxyUrl=${encodeURIComponent(url)}`; 47 | return fetch(newURL, options); 48 | }; 49 | 50 | /** 51 | * Searches for an author by name using the Semantic Scholar API. 52 | * @param query - The name of the author to search for. 53 | * @returns A promise that resolves to the search response containing author information. 54 | * @throws Error if the search request fails. 55 | */ 56 | export const searchAuthorByName = async ( 57 | query: string 58 | ): Promise => { 59 | const baseURL = 'https://api.semanticscholar.org/graph/v1/author/search'; 60 | const url = `${baseURL}?query=${encodeURIComponent(query)}`; 61 | const result = await proxyFetch(url); 62 | if (!result.ok) { 63 | throw Error(`Search request failed with status: ${result.statusText}`); 64 | } 65 | 66 | const data = (await result.json()) as SemanticAuthorSearchResponse; 67 | return data; 68 | }; 69 | 70 | /** 71 | * Retrieves details for multiple authors using the Semantic Scholar API. 72 | * @param authors - An array of author IDs. 73 | * @returns A promise that resolves to an array of author details. 74 | * @throws Error if the fetch request fails. 75 | */ 76 | export const searchAuthorDetails = async ( 77 | authorIDs: string[], 78 | fields?: string 79 | ): Promise => { 80 | if (fields !== undefined && MOCK_HTTP_CALL) { 81 | const result = authorCitationSearchMocks[authorCitationMockCounter]; 82 | authorCitationMockCounter = 83 | (authorCitationMockCounter + 1) % authorCitationSearchMocks.length; 84 | return result; 85 | } 86 | 87 | // Prepare for the fetch 88 | const body = { 89 | ids: authorIDs 90 | }; 91 | 92 | const baseURL = 'https://api.semanticscholar.org/graph/v1/author/batch'; 93 | 94 | const curFields = 95 | fields || 'authorId,name,affiliations,homepage,paperCount,citationCount'; 96 | const parameters: Record = { 97 | fields: curFields 98 | }; 99 | const encodedParameters = new URLSearchParams(parameters); 100 | 101 | const options: RequestInit = { 102 | method: 'POST', 103 | headers: { 104 | 'Content-Type': 'application/json' 105 | }, 106 | body: JSON.stringify(body) 107 | }; 108 | 109 | const url = `${baseURL}?${encodedParameters.toString()}`; 110 | 111 | // Fetch the author details 112 | try { 113 | const response = await proxyFetch(url, options); 114 | const data = (await response.json()) as SemanticAuthorDetail[]; 115 | 116 | // downloadJSON(data, null, 'author.json'); 117 | 118 | return data; 119 | } catch (e) { 120 | console.error('API error', e); 121 | throw new Error(`Fetch error when getting author details, status: ${e}`); 122 | } 123 | }; 124 | 125 | export const getAllPapersFromAuthor = async (authorID: string) => { 126 | if (MOCK_HTTP_CALL) { 127 | return paperSearchMock.data; 128 | } 129 | 130 | // Prepare for the fetch 131 | const baseURL = `https://api.semanticscholar.org/graph/v1/author/${authorID}/papers`; 132 | let offset = 0; 133 | let isComplete = false; 134 | const papers: SemanticPaper[] = []; 135 | 136 | while (!isComplete) { 137 | const parameters: Record = { 138 | fields: 139 | 'corpusId,url,title,venue,publicationVenue,year,authors,externalIds,citationCount,publicationDate', 140 | offset: String(offset) 141 | }; 142 | const encodedParameters = new URLSearchParams(parameters); 143 | const url = `${baseURL}?${encodedParameters.toString()}`; 144 | 145 | // Fetch the paper details 146 | const response = await proxyFetch(url); 147 | if (!response.ok) { 148 | throw Error( 149 | `Fetch error when getting paper details, status: ${response.status}` 150 | ); 151 | } 152 | 153 | const data = (await response.json()) as SemanticPaperSearchResponse; 154 | 155 | // Append the paper data 156 | data.data.forEach(d => { 157 | papers.push(d); 158 | }); 159 | 160 | // Keep fetching until retrieving all the papers 161 | if (data.next !== undefined) { 162 | offset = data.next; 163 | } else { 164 | isComplete = true; 165 | } 166 | } 167 | 168 | return papers; 169 | }; 170 | 171 | export const getPaperCitations = async (paperIDs: string[]) => { 172 | if (MOCK_HTTP_CALL) { 173 | return citationSearchMock; 174 | } 175 | 176 | // Prepare for the fetch 177 | const body = { 178 | ids: paperIDs 179 | }; 180 | 181 | const baseURL = 'https://api.semanticscholar.org/graph/v1/paper/batch'; 182 | const parameters: Record = { 183 | fields: 'paperId,citations.authors,citations.title' 184 | }; 185 | const encodedParameters = new URLSearchParams(parameters); 186 | 187 | const options: RequestInit = { 188 | method: 'POST', 189 | headers: { 190 | 'Content-Type': 'application/json' 191 | }, 192 | body: JSON.stringify(body) 193 | }; 194 | 195 | const url = `${baseURL}?${encodedParameters.toString()}`; 196 | 197 | // Fetch the author details 198 | const response = await proxyFetch(url, options); 199 | if (!response.ok) { 200 | throw Error( 201 | `Fetch error when getting author details, status: ${response.status}` 202 | ); 203 | } 204 | 205 | const data = (await response.json()) as SemanticPaperCitationDetail[]; 206 | return data; 207 | }; 208 | -------------------------------------------------------------------------------- /src/components/author-view/author-view.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | import { SlInput } from '@shoelace-style/shoelace'; 5 | import { searchAuthorByName } from '../../api/semantic-scholar'; 6 | import type { 7 | SemanticAuthorSearchResponse, 8 | SemanticAuthorSearch, 9 | SemanticAuthorDetail 10 | } from '../../types/common-types'; 11 | import type { RecRecAuthorList } from '../author-list/author-list'; 12 | 13 | import '@shoelace-style/shoelace/dist/components/input/input.js'; 14 | import '../header-bar/header-bar'; 15 | import '../author-list/author-list'; 16 | 17 | import '@shoelace-style/shoelace/dist/themes/light.css'; 18 | import componentCSS from './author-view.css?inline'; 19 | import iconSearch from '../../images/icon-search.svg?raw'; 20 | 21 | /** 22 | * Author view element. 23 | */ 24 | @customElement('recrec-author-view') 25 | export class RecRecAuthorView extends LitElement { 26 | //==========================================================================|| 27 | // Class Properties || 28 | //==========================================================================|| 29 | selectedProfile: SemanticAuthorDetail | null = null; 30 | searchAuthorTimer: number | null = null; 31 | lastCompletedQuery: string = ''; 32 | 33 | @state() 34 | searchAuthors: SemanticAuthorSearch[] = []; 35 | 36 | @state() 37 | showAuthorList = false; 38 | 39 | @state() 40 | isSearching = false; 41 | 42 | @query('sl-input') 43 | searchInputComponent: SlInput | undefined; 44 | 45 | @query('recrec-author-list') 46 | authorListComponent: RecRecAuthorList | undefined; 47 | 48 | //==========================================================================|| 49 | // Lifecycle Methods || 50 | //==========================================================================|| 51 | constructor() { 52 | super(); 53 | } 54 | 55 | /** 56 | * This method is called when the DOM is added for the first time 57 | */ 58 | firstUpdated() {} 59 | 60 | /** 61 | * This method is called before new DOM is updated and rendered 62 | * @param changedProperties Property that has been changed 63 | */ 64 | willUpdate(changedProperties: PropertyValues) {} 65 | 66 | //==========================================================================|| 67 | // Custom Methods || 68 | //==========================================================================|| 69 | async initData() {} 70 | 71 | //==========================================================================|| 72 | // Event Handlers || 73 | //==========================================================================|| 74 | searchInput(e: InputEvent, delay = 600) { 75 | const target = e.currentTarget as HTMLInputElement; 76 | const query = target.value; 77 | 78 | if (query === '') { 79 | this.searchAuthors = []; 80 | this.lastCompletedQuery = ''; 81 | this.showAuthorList = false; 82 | return; 83 | } 84 | 85 | // Debounce the search 86 | if (this.searchAuthorTimer !== null) { 87 | clearTimeout(this.searchAuthorTimer); 88 | } 89 | 90 | this.searchAuthorTimer = setTimeout(() => { 91 | this.updateAuthorList(query).then( 92 | () => {}, 93 | () => {} 94 | ); 95 | }, delay); 96 | } 97 | 98 | searchFocused() { 99 | // Show the author list if the query is not empty 100 | if (!this.searchInputComponent) { 101 | return; 102 | } 103 | 104 | const query = this.searchInputComponent.value; 105 | if (this.lastCompletedQuery === query && query !== '') { 106 | this.showAuthorList = true; 107 | } 108 | } 109 | 110 | searchBlurred() { 111 | setTimeout(() => { 112 | this.showAuthorList = false; 113 | }, 200); 114 | } 115 | 116 | //==========================================================================|| 117 | // Private Helpers || 118 | //==========================================================================|| 119 | async updateAuthorList(query: string) { 120 | // Skip the query if it is the same as the last query 121 | if (query === this.lastCompletedQuery) { 122 | return; 123 | } 124 | 125 | const data = await searchAuthorByName(query); 126 | 127 | // Pass the author info to the author list component 128 | this.searchAuthors = data.data; 129 | this.lastCompletedQuery = query; 130 | 131 | if (this.searchAuthors.length > 0) { 132 | this.showAuthorList = true; 133 | } else { 134 | this.showAuthorList = false; 135 | } 136 | } 137 | 138 | authorRowClickedHandler(e: CustomEvent) { 139 | this.selectedProfile = e.detail; 140 | 141 | // Also fill the input with the selected author 142 | if (this.searchInputComponent) { 143 | this.searchInputComponent.value = this.selectedProfile!.name!; 144 | } 145 | } 146 | 147 | confirmButtonClicked() { 148 | if (this.selectedProfile === null) { 149 | return; 150 | } 151 | 152 | const event = new Event('confirm-button-clicked', { 153 | bubbles: true, 154 | composed: true 155 | }); 156 | this.dispatchEvent(event); 157 | } 158 | 159 | //==========================================================================|| 160 | // Templates and Styles || 161 | //==========================================================================|| 162 | render() { 163 | return html` 164 |
165 |
166 | 192 | 193 |
194 | ) => { 197 | this.authorRowClickedHandler(e); 198 | }} 199 | @is-searching-changed=${(e: CustomEvent) => { 200 | this.isSearching = e.detail; 201 | }} 202 | > 203 |
204 |
205 | 206 | 215 |
216 | `; 217 | } 218 | 219 | static styles = [ 220 | css` 221 | ${unsafeCSS(componentCSS)} 222 | ` 223 | ]; 224 | } 225 | 226 | declare global { 227 | interface HTMLElementTagNameMap { 228 | 'recrec-author-view': RecRecAuthorView; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/components/recrec/recrec.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | import { 5 | Step, 6 | SemanticAuthorDetail, 7 | SemanticPaper 8 | } from '../../types/common-types'; 9 | import { RecRecPaperView } from '../paper-view/paper-view'; 10 | 11 | import '../author-view/author-view'; 12 | import '../paper-view/paper-view'; 13 | import '../recommender-view/recommender-view'; 14 | 15 | import componentCSS from './recrec.css?inline'; 16 | import iconBars from '../../images/icon-bars.svg?raw'; 17 | 18 | const MOBILE_MODE = window.screen.width < 768; 19 | const steps = [Step.Author, Step.Paper, Step.Recommender]; 20 | 21 | /** 22 | * App element. 23 | * 24 | */ 25 | @customElement('recrec-app') 26 | export class RecRecApp extends LitElement { 27 | //==========================================================================|| 28 | // Class Properties || 29 | //==========================================================================|| 30 | @state() 31 | curStep: Step = Step.Author; 32 | 33 | @state() 34 | selectedProfile: SemanticAuthorDetail | null = null; 35 | 36 | @query('recrec-paper-view') 37 | paperViewComponent!: RecRecPaperView; 38 | 39 | @state() 40 | papers: SemanticPaper[] = []; 41 | 42 | @state() 43 | selectedPaperIDs = new Set(); 44 | 45 | @state() 46 | confirmedSelectedPaperIDs = new Set(); 47 | 48 | @state() 49 | showPopMenu = false; 50 | 51 | //==========================================================================|| 52 | // Lifecycle Methods || 53 | //==========================================================================|| 54 | constructor() { 55 | super(); 56 | // this.selectedProfile = { 57 | // authorId: '1390877819', 58 | // name: 'Zijie J. Wang', 59 | // affiliations: ['Georgia Tech'], 60 | // homepage: 'https://zijie.wang', 61 | // paperCount: 42, 62 | // citationCount: 1716 63 | // }; 64 | } 65 | 66 | /** 67 | * This method is called before new DOM is updated and rendered 68 | * @param changedProperties Property that has been changed 69 | */ 70 | willUpdate(changedProperties: PropertyValues) {} 71 | 72 | //==========================================================================|| 73 | // Custom Methods || 74 | //==========================================================================|| 75 | async initData() {} 76 | 77 | //==========================================================================|| 78 | // Event Handlers || 79 | //==========================================================================|| 80 | authorRowClickedHandler(e: CustomEvent) { 81 | this.selectedProfile = e.detail; 82 | } 83 | 84 | selectedPaperCountUpdatedHandler(e: CustomEvent>) { 85 | this.selectedPaperIDs = e.detail; 86 | } 87 | 88 | papersUpdatedHandler(e: CustomEvent) { 89 | this.papers = e.detail; 90 | } 91 | 92 | headerStepClickedHandler(e: CustomEvent<'pre' | 'next'>) { 93 | const curStepIndex = steps.indexOf(this.curStep); 94 | if (e.detail === 'pre') { 95 | const newCurStepIndex = Math.max(0, curStepIndex - 1); 96 | this.curStep = steps[newCurStepIndex]; 97 | } else { 98 | const newCurStepIndex = Math.min(steps.length - 1, curStepIndex + 1); 99 | this.curStep = steps[newCurStepIndex]; 100 | } 101 | 102 | if (this.curStep === Step.Recommender) { 103 | this.confirmedSelectedPaperIDs = this.selectedPaperIDs; 104 | } 105 | } 106 | 107 | //==========================================================================|| 108 | // Private Helpers || 109 | //==========================================================================|| 110 | moveToNextStep() { 111 | const curIndex = steps.indexOf(this.curStep); 112 | if (curIndex + 1 >= steps.length) { 113 | throw Error(`There is no more step after this step: ${this.curStep}`); 114 | } 115 | this.curStep = steps[curIndex + 1]; 116 | 117 | if (this.curStep === Step.Recommender) { 118 | this.confirmedSelectedPaperIDs = this.selectedPaperIDs; 119 | } 120 | } 121 | 122 | jumpToStep(step: Step) { 123 | this.curStep = step; 124 | } 125 | 126 | //==========================================================================|| 127 | // Templates and Styles || 128 | //==========================================================================|| 129 | render() { 130 | // Render the content view based on the current step 131 | const contentView = html` 132 | ) => { 136 | this.authorRowClickedHandler(e); 137 | }} 138 | @confirm-button-clicked=${() => { 139 | this.moveToNextStep(); 140 | }} 141 | > 142 | 143 | { 148 | this.moveToNextStep(); 149 | }} 150 | @selected-paper-count-updated=${(e: CustomEvent>) => { 151 | this.selectedPaperCountUpdatedHandler(e); 152 | }} 153 | @papers-updated=${(e: CustomEvent) => { 154 | this.papersUpdatedHandler(e); 155 | }} 156 | > 157 | 158 | 167 | `; 168 | 169 | // Compile a menu icon for the mobile version 170 | const menuButton = html` 171 |
172 | 184 |
185 | `; 186 | 187 | return html` 188 |
189 |
190 | ) => { 194 | this.headerStepClickedHandler(e); 195 | }} 196 | > 197 | 198 |
199 | ${MOBILE_MODE && this.curStep === Step.Recommender 200 | ? menuButton 201 | : html``} 202 | 203 |
204 | ${MOBILE_MODE ? "I'm" : 'My Profile:'} 205 | 216 |
217 | 218 |
219 | ${MOBILE_MODE ? 'Papers' : 'Representative Papers:'} 220 | 228 |
229 |
230 | 231 |
${contentView}
232 |
233 |
234 | `; 235 | } 236 | 237 | static styles = [ 238 | css` 239 | ${unsafeCSS(componentCSS)} 240 | ` 241 | ]; 242 | } 243 | 244 | declare global { 245 | interface HTMLElementTagNameMap { 246 | 'recrec-app': RecRecApp; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/recrec-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | "jsx": "react-jsx" /* Specify what JSX code is generated. */, 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "es2022" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | "types": [ 35 | "@cloudflare/workers-types/2023-07-01" 36 | ] /* Specify type package names to be included without being referenced in a source file. */, 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | "resolveJsonModule": true /* Enable importing .json files */, 39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 43 | "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | "noEmit": true /* Disable emitting files from a compilation. */, 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 73 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 74 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 81 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 86 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | const layout = { 2 | sidebarMenuXOffset: 16 3 | }; 4 | 5 | const time = { 6 | mouseenterDelay: 300 7 | }; 8 | 9 | const customColors = { 10 | addedColor: 'hsl(120, 37%, 88%)', 11 | replacedColor: 'hsl(35, 100%, 89%)', 12 | deletedColor: 'hsl(353, 100%, 93%)' 13 | }; 14 | 15 | const colors = { 16 | 'red-50': 'hsl(350, 100.0%, 96.08%)', 17 | 'red-100': 'hsl(354, 100.0%, 90.2%)', 18 | 'red-200': 'hsl(0, 72.65%, 77.06%)', 19 | 'red-300': 'hsl(0, 68.67%, 67.45%)', 20 | 'red-400': 'hsl(1, 83.25%, 62.55%)', 21 | 'red-500': 'hsl(4, 89.62%, 58.43%)', 22 | 'red-600': 'hsl(1, 77.19%, 55.29%)', 23 | 'red-700': 'hsl(0, 65.08%, 50.59%)', 24 | 'red-800': 'hsl(0, 66.39%, 46.67%)', 25 | 'red-900': 'hsl(0, 73.46%, 41.37%)', 26 | 'red-a100': 'hsl(4, 100.0%, 75.1%)', 27 | 'red-a200': 'hsl(0, 100.0%, 66.08%)', 28 | 'red-a400': 'hsl(348, 100.0%, 54.51%)', 29 | 'red-a700': 'hsl(0, 100.0%, 41.76%)', 30 | 31 | 'pink-50': 'hsl(340, 80.0%, 94.12%)', 32 | 'pink-100': 'hsl(339, 81.33%, 85.29%)', 33 | 'pink-200': 'hsl(339, 82.11%, 75.88%)', 34 | 'pink-300': 'hsl(339, 82.56%, 66.27%)', 35 | 'pink-400': 'hsl(339, 81.9%, 58.82%)', 36 | 'pink-500': 'hsl(339, 82.19%, 51.57%)', 37 | 'pink-600': 'hsl(338, 77.78%, 47.65%)', 38 | 'pink-700': 'hsl(336, 77.98%, 42.75%)', 39 | 'pink-800': 'hsl(333, 79.27%, 37.84%)', 40 | 'pink-900': 'hsl(328, 81.33%, 29.41%)', 41 | 'pink-a100': 'hsl(339, 100.0%, 75.1%)', 42 | 'pink-a200': 'hsl(339, 100.0%, 62.55%)', 43 | 'pink-a400': 'hsl(338, 100.0%, 48.04%)', 44 | 'pink-a700': 'hsl(333, 84.11%, 41.96%)', 45 | 46 | 'purple-50': 'hsl(292, 44.44%, 92.94%)', 47 | 'purple-100': 'hsl(291, 46.07%, 82.55%)', 48 | 'purple-200': 'hsl(291, 46.94%, 71.18%)', 49 | 'purple-300': 'hsl(291, 46.6%, 59.61%)', 50 | 'purple-400': 'hsl(291, 46.61%, 50.78%)', 51 | 'purple-500': 'hsl(291, 63.72%, 42.16%)', 52 | 'purple-600': 'hsl(287, 65.05%, 40.39%)', 53 | 'purple-700': 'hsl(282, 67.88%, 37.84%)', 54 | 'purple-800': 'hsl(277, 70.17%, 35.49%)', 55 | 'purple-900': 'hsl(267, 75.0%, 31.37%)', 56 | 'purple-a100': 'hsl(291, 95.38%, 74.51%)', 57 | 'purple-a200': 'hsl(291, 95.9%, 61.76%)', 58 | 'purple-a400': 'hsl(291, 100.0%, 48.82%)', 59 | 'purple-a700': 'hsl(280, 100.0%, 50.0%)', 60 | 61 | 'deep-purple-50': 'hsl(264, 45.45%, 93.53%)', 62 | 'deep-purple-100': 'hsl(261, 45.68%, 84.12%)', 63 | 'deep-purple-200': 'hsl(261, 46.27%, 73.73%)', 64 | 'deep-purple-300': 'hsl(261, 46.81%, 63.14%)', 65 | 'deep-purple-400': 'hsl(261, 46.72%, 55.1%)', 66 | 'deep-purple-500': 'hsl(261, 51.87%, 47.25%)', 67 | 'deep-purple-600': 'hsl(259, 53.91%, 45.1%)', 68 | 'deep-purple-700': 'hsl(257, 57.75%, 41.76%)', 69 | 'deep-purple-800': 'hsl(254, 60.8%, 39.02%)', 70 | 'deep-purple-900': 'hsl(251, 68.79%, 33.92%)', 71 | 'deep-purple-a100': 'hsl(261, 100.0%, 76.67%)', 72 | 'deep-purple-a200': 'hsl(255, 100.0%, 65.1%)', 73 | 'deep-purple-a400': 'hsl(258, 100.0%, 56.08%)', 74 | 'deep-purple-a700': 'hsl(265, 100.0%, 45.88%)', 75 | 76 | 'indigo-50': 'hsl(231, 43.75%, 93.73%)', 77 | 'indigo-100': 'hsl(231, 45.0%, 84.31%)', 78 | 'indigo-200': 'hsl(230, 44.36%, 73.92%)', 79 | 'indigo-300': 'hsl(230, 44.09%, 63.53%)', 80 | 'indigo-400': 'hsl(230, 44.25%, 55.69%)', 81 | 'indigo-500': 'hsl(230, 48.36%, 47.84%)', 82 | 'indigo-600': 'hsl(231, 50.0%, 44.71%)', 83 | 'indigo-700': 'hsl(231, 53.62%, 40.59%)', 84 | 'indigo-800': 'hsl(232, 57.22%, 36.67%)', 85 | 'indigo-900': 'hsl(234, 65.79%, 29.8%)', 86 | 'indigo-a100': 'hsl(230, 100.0%, 77.45%)', 87 | 'indigo-a200': 'hsl(230, 98.84%, 66.08%)', 88 | 'indigo-a400': 'hsl(230, 98.97%, 61.76%)', 89 | 'indigo-a700': 'hsl(230, 99.04%, 59.22%)', 90 | 91 | 'blue-50': 'hsl(205, 86.67%, 94.12%)', 92 | 'blue-100': 'hsl(207, 88.89%, 85.88%)', 93 | 'blue-200': 'hsl(206, 89.74%, 77.06%)', 94 | 'blue-300': 'hsl(206, 89.02%, 67.84%)', 95 | 'blue-400': 'hsl(206, 89.95%, 60.98%)', 96 | 'blue-500': 'hsl(206, 89.74%, 54.12%)', 97 | 'blue-600': 'hsl(208, 79.28%, 50.78%)', 98 | 'blue-700': 'hsl(209, 78.72%, 46.08%)', 99 | 'blue-800': 'hsl(211, 80.28%, 41.76%)', 100 | 'blue-900': 'hsl(216, 85.06%, 34.12%)', 101 | 'blue-a100': 'hsl(217, 100.0%, 75.49%)', 102 | 'blue-a200': 'hsl(217, 100.0%, 63.33%)', 103 | 'blue-a400': 'hsl(217, 100.0%, 58.04%)', 104 | 'blue-a700': 'hsl(224, 100.0%, 58.04%)', 105 | 106 | 'light-blue-50': 'hsl(198, 93.55%, 93.92%)', 107 | 'light-blue-100': 'hsl(198, 92.41%, 84.51%)', 108 | 'light-blue-200': 'hsl(198, 92.37%, 74.31%)', 109 | 'light-blue-300': 'hsl(198, 91.3%, 63.92%)', 110 | 'light-blue-400': 'hsl(198, 91.93%, 56.27%)', 111 | 'light-blue-500': 'hsl(198, 97.57%, 48.43%)', 112 | 'light-blue-600': 'hsl(199, 97.41%, 45.49%)', 113 | 'light-blue-700': 'hsl(201, 98.1%, 41.37%)', 114 | 'light-blue-800': 'hsl(202, 97.91%, 37.45%)', 115 | 'light-blue-900': 'hsl(206, 98.72%, 30.59%)', 116 | 'light-blue-a100': 'hsl(198, 100.0%, 75.1%)', 117 | 'light-blue-a200': 'hsl(198, 100.0%, 62.55%)', 118 | 'light-blue-a400': 'hsl(198, 100.0%, 50.0%)', 119 | 'light-blue-a700': 'hsl(202, 100.0%, 45.88%)', 120 | 121 | 'cyan-50': 'hsl(186, 72.22%, 92.94%)', 122 | 'cyan-100': 'hsl(186, 71.11%, 82.35%)', 123 | 'cyan-200': 'hsl(186, 71.62%, 70.98%)', 124 | 'cyan-300': 'hsl(186, 71.15%, 59.22%)', 125 | 'cyan-400': 'hsl(186, 70.87%, 50.2%)', 126 | 'cyan-500': 'hsl(186, 100.0%, 41.57%)', 127 | 'cyan-600': 'hsl(186, 100.0%, 37.84%)', 128 | 'cyan-700': 'hsl(185, 100.0%, 32.75%)', 129 | 'cyan-800': 'hsl(185, 100.0%, 28.04%)', 130 | 'cyan-900': 'hsl(182, 100.0%, 19.61%)', 131 | 'cyan-a100': 'hsl(180, 100.0%, 75.88%)', 132 | 'cyan-a200': 'hsl(180, 100.0%, 54.71%)', 133 | 'cyan-a400': 'hsl(186, 100.0%, 50.0%)', 134 | 'cyan-a700': 'hsl(187, 100.0%, 41.57%)', 135 | 136 | 'teal-50': 'hsl(176, 40.91%, 91.37%)', 137 | 'teal-100': 'hsl(174, 41.28%, 78.63%)', 138 | 'teal-200': 'hsl(174, 41.9%, 64.9%)', 139 | 'teal-300': 'hsl(174, 41.83%, 50.78%)', 140 | 'teal-400': 'hsl(174, 62.75%, 40.0%)', 141 | 'teal-500': 'hsl(174, 100.0%, 29.41%)', 142 | 'teal-600': 'hsl(173, 100.0%, 26.86%)', 143 | 'teal-700': 'hsl(173, 100.0%, 23.73%)', 144 | 'teal-800': 'hsl(172, 100.0%, 20.59%)', 145 | 'teal-900': 'hsl(169, 100.0%, 15.1%)', 146 | 'teal-a100': 'hsl(166, 100.0%, 82.75%)', 147 | 'teal-a200': 'hsl(165, 100.0%, 69.61%)', 148 | 'teal-a400': 'hsl(165, 82.26%, 51.37%)', 149 | 'teal-a700': 'hsl(171, 100.0%, 37.45%)', 150 | 151 | 'green-50': 'hsl(124, 39.39%, 93.53%)', 152 | 'green-100': 'hsl(121, 37.5%, 84.31%)', 153 | 'green-200': 'hsl(122, 37.4%, 74.31%)', 154 | 'green-300': 'hsl(122, 38.46%, 64.31%)', 155 | 'green-400': 'hsl(122, 38.46%, 56.67%)', 156 | 'green-500': 'hsl(122, 39.44%, 49.22%)', 157 | 'green-600': 'hsl(122, 40.97%, 44.51%)', 158 | 'green-700': 'hsl(122, 43.43%, 38.82%)', 159 | 'green-800': 'hsl(123, 46.2%, 33.53%)', 160 | 'green-900': 'hsl(124, 55.37%, 23.73%)', 161 | 'green-a100': 'hsl(136, 77.22%, 84.51%)', 162 | 'green-a200': 'hsl(150, 81.82%, 67.65%)', 163 | 'green-a400': 'hsl(150, 100.0%, 45.1%)', 164 | 'green-a700': 'hsl(144, 100.0%, 39.22%)', 165 | 166 | 'light-green-50': 'hsl(88, 51.72%, 94.31%)', 167 | 'light-green-100': 'hsl(87, 50.68%, 85.69%)', 168 | 'light-green-200': 'hsl(88, 50.0%, 76.47%)', 169 | 'light-green-300': 'hsl(87, 50.0%, 67.06%)', 170 | 'light-green-400': 'hsl(87, 50.24%, 59.8%)', 171 | 'light-green-500': 'hsl(87, 50.21%, 52.75%)', 172 | 'light-green-600': 'hsl(89, 46.12%, 48.04%)', 173 | 'light-green-700': 'hsl(92, 47.91%, 42.16%)', 174 | 'light-green-800': 'hsl(95, 49.46%, 36.47%)', 175 | 'light-green-900': 'hsl(103, 55.56%, 26.47%)', 176 | 'light-green-a100': 'hsl(87, 100.0%, 78.24%)', 177 | 'light-green-a200': 'hsl(87, 100.0%, 67.45%)', 178 | 'light-green-a400': 'hsl(92, 100.0%, 50.59%)', 179 | 'light-green-a700': 'hsl(96, 81.15%, 47.84%)', 180 | 181 | 'lime-50': 'hsl(65, 71.43%, 94.51%)', 182 | 'lime-100': 'hsl(64, 69.01%, 86.08%)', 183 | 'lime-200': 'hsl(65, 70.69%, 77.25%)', 184 | 'lime-300': 'hsl(65, 70.37%, 68.24%)', 185 | 'lime-400': 'hsl(65, 69.7%, 61.18%)', 186 | 'lime-500': 'hsl(65, 69.96%, 54.31%)', 187 | 'lime-600': 'hsl(63, 59.68%, 49.61%)', 188 | 'lime-700': 'hsl(62, 61.43%, 43.73%)', 189 | 'lime-800': 'hsl(59, 62.89%, 38.04%)', 190 | 'lime-900': 'hsl(53, 69.93%, 30.0%)', 191 | 'lime-a100': 'hsl(65, 100.0%, 75.29%)', 192 | 'lime-a200': 'hsl(65, 100.0%, 62.75%)', 193 | 'lime-a400': 'hsl(73, 100.0%, 50.0%)', 194 | 'lime-a700': 'hsl(75, 100.0%, 45.88%)', 195 | 196 | 'yellow-50': 'hsl(55, 100.0%, 95.29%)', 197 | 'yellow-100': 'hsl(53, 100.0%, 88.43%)', 198 | 'yellow-200': 'hsl(53, 100.0%, 80.78%)', 199 | 'yellow-300': 'hsl(53, 100.0%, 73.14%)', 200 | 'yellow-400': 'hsl(53, 100.0%, 67.25%)', 201 | 'yellow-500': 'hsl(53, 100.0%, 61.57%)', 202 | 'yellow-600': 'hsl(48, 98.04%, 60.0%)', 203 | 'yellow-700': 'hsl(42, 96.26%, 58.04%)', 204 | 'yellow-800': 'hsl(37, 94.64%, 56.08%)', 205 | 'yellow-900': 'hsl(28, 91.74%, 52.55%)', 206 | 'yellow-a100': 'hsl(60, 100.0%, 77.65%)', 207 | 'yellow-a200': 'hsl(60, 100.0%, 50.0%)', 208 | 'yellow-a400': 'hsl(55, 100.0%, 50.0%)', 209 | 'yellow-a700': 'hsl(50, 100.0%, 50.0%)', 210 | 211 | 'amber-50': 'hsl(46, 100.0%, 94.12%)', 212 | 'amber-100': 'hsl(45, 100.0%, 85.1%)', 213 | 'amber-200': 'hsl(45, 100.0%, 75.49%)', 214 | 'amber-300': 'hsl(45, 100.0%, 65.49%)', 215 | 'amber-400': 'hsl(45, 100.0%, 57.84%)', 216 | 'amber-500': 'hsl(45, 100.0%, 51.37%)', 217 | 'amber-600': 'hsl(42, 100.0%, 50.0%)', 218 | 'amber-700': 'hsl(37, 100.0%, 50.0%)', 219 | 'amber-800': 'hsl(33, 100.0%, 50.0%)', 220 | 'amber-900': 'hsl(26, 100.0%, 50.0%)', 221 | 'amber-a100': 'hsl(47, 100.0%, 74.9%)', 222 | 'amber-a200': 'hsl(47, 100.0%, 62.55%)', 223 | 'amber-a400': 'hsl(46, 100.0%, 50.0%)', 224 | 'amber-a700': 'hsl(40, 100.0%, 50.0%)', 225 | 226 | 'orange-50': 'hsl(36, 100.0%, 93.92%)', 227 | 'orange-100': 'hsl(35, 100.0%, 84.9%)', 228 | 'orange-200': 'hsl(35, 100.0%, 75.1%)', 229 | 'orange-300': 'hsl(35, 100.0%, 65.1%)', 230 | 'orange-400': 'hsl(35, 100.0%, 57.45%)', 231 | 'orange-500': 'hsl(35, 100.0%, 50.0%)', 232 | 'orange-600': 'hsl(33, 100.0%, 49.22%)', 233 | 'orange-700': 'hsl(30, 100.0%, 48.04%)', 234 | 'orange-800': 'hsl(27, 100.0%, 46.86%)', 235 | 'orange-900': 'hsl(21, 100.0%, 45.1%)', 236 | 'orange-a100': 'hsl(38, 100.0%, 75.1%)', 237 | 'orange-a200': 'hsl(33, 100.0%, 62.55%)', 238 | 'orange-a400': 'hsl(34, 100.0%, 50.0%)', 239 | 'orange-a700': 'hsl(25, 100.0%, 50.0%)', 240 | 241 | 'deep-orange-50': 'hsl(5, 71.43%, 94.51%)', 242 | 'deep-orange-100': 'hsl(14, 100.0%, 86.86%)', 243 | 'deep-orange-200': 'hsl(14, 100.0%, 78.43%)', 244 | 'deep-orange-300': 'hsl(14, 100.0%, 69.8%)', 245 | 'deep-orange-400': 'hsl(14, 100.0%, 63.14%)', 246 | 'deep-orange-500': 'hsl(14, 100.0%, 56.67%)', 247 | 'deep-orange-600': 'hsl(14, 90.68%, 53.73%)', 248 | 'deep-orange-700': 'hsl(14, 80.39%, 50.0%)', 249 | 'deep-orange-800': 'hsl(14, 82.28%, 46.47%)', 250 | 'deep-orange-900': 'hsl(14, 88.18%, 39.8%)', 251 | 'deep-orange-a100': 'hsl(14, 100.0%, 75.1%)', 252 | 'deep-orange-a200': 'hsl(14, 100.0%, 62.55%)', 253 | 'deep-orange-a400': 'hsl(14, 100.0%, 50.0%)', 254 | 'deep-orange-a700': 'hsl(11, 100.0%, 43.33%)', 255 | 256 | 'brown-50': 'hsl(19, 15.79%, 92.55%)', 257 | 'brown-100': 'hsl(16, 15.79%, 81.37%)', 258 | 'brown-200': 'hsl(14, 15.19%, 69.02%)', 259 | 'brown-300': 'hsl(15, 15.32%, 56.47%)', 260 | 'brown-400': 'hsl(15, 17.5%, 47.06%)', 261 | 'brown-500': 'hsl(15, 25.39%, 37.84%)', 262 | 'brown-600': 'hsl(15, 25.29%, 34.12%)', 263 | 'brown-700': 'hsl(14, 25.68%, 29.02%)', 264 | 'brown-800': 'hsl(11, 25.81%, 24.31%)', 265 | 'brown-900': 'hsl(8, 27.84%, 19.02%)', 266 | 267 | 'gray-50': 'hsl(0, 0.0%, 98.04%)', 268 | 'gray-100': 'hsl(0, 0.0%, 96.08%)', 269 | 'gray-200': 'hsl(0, 0.0%, 93.33%)', 270 | 'gray-300': 'hsl(0, 0.0%, 87.84%)', 271 | 'gray-400': 'hsl(0, 0.0%, 74.12%)', 272 | 'gray-500': 'hsl(0, 0.0%, 61.96%)', 273 | 'gray-600': 'hsl(0, 0.0%, 45.88%)', 274 | 'gray-700': 'hsl(0, 0.0%, 38.04%)', 275 | 'gray-800': 'hsl(0, 0.0%, 25.88%)', 276 | 'gray-900': 'hsl(0, 0.0%, 12.94%)', 277 | 278 | 'blue-gray-50': 'hsl(204, 15.15%, 93.53%)', 279 | 'blue-gray-100': 'hsl(198, 15.66%, 83.73%)', 280 | 'blue-gray-200': 'hsl(199, 15.33%, 73.14%)', 281 | 'blue-gray-300': 'hsl(199, 15.63%, 62.35%)', 282 | 'blue-gray-400': 'hsl(200, 15.38%, 54.12%)', 283 | 'blue-gray-500': 'hsl(199, 18.3%, 46.08%)', 284 | 'blue-gray-600': 'hsl(198, 18.45%, 40.39%)', 285 | 'blue-gray-700': 'hsl(199, 18.34%, 33.14%)', 286 | 'blue-gray-800': 'hsl(199, 17.91%, 26.27%)', 287 | 'blue-gray-900': 'hsl(199, 19.15%, 18.43%)', 288 | 'blue-gray-1000': 'hsl(199, 20.93%, 8.43%)' 289 | }; 290 | 291 | export const config = { 292 | colors, 293 | layout, 294 | time, 295 | customColors, 296 | debug: true 297 | }; 298 | -------------------------------------------------------------------------------- /src/components/paper-view/paper-view.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, unsafeCSS, html, PropertyValues } from 'lit'; 2 | import { customElement, property, state, query } from 'lit/decorators.js'; 3 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 4 | import { 5 | Step, 6 | SemanticAuthorDetail, 7 | SemanticPaper 8 | } from '../../types/common-types'; 9 | import { getAllPapersFromAuthor } from '../../api/semantic-scholar'; 10 | 11 | import componentCSS from './paper-view.css?inline'; 12 | 13 | const MOBILE_MODE = window.screen.width < 768; 14 | 15 | /** 16 | * Paper view element. 17 | */ 18 | @customElement('recrec-paper-view') 19 | export class RecRecPaperView extends LitElement { 20 | //==========================================================================|| 21 | // Class Properties || 22 | //==========================================================================|| 23 | @property({ attribute: false }) 24 | selectedProfile: SemanticAuthorDetail | null = null; 25 | 26 | @state() 27 | papers: SemanticPaper[] = []; 28 | 29 | @state() 30 | selectedPaperIDs: Set = new Set(); 31 | 32 | @state() 33 | isCompleted = false; 34 | 35 | lastClickedHeader: '' | 'title' | 'citedBy' | 'year' = ''; 36 | 37 | //==========================================================================|| 38 | // Lifecycle Methods || 39 | //==========================================================================|| 40 | constructor() { 41 | super(); 42 | } 43 | 44 | /** 45 | * This method is called when the DOM is added for the first time 46 | */ 47 | firstUpdated() {} 48 | 49 | /** 50 | * This method is called before new DOM is updated and rendered 51 | * @param changedProperties Property that has been changed 52 | */ 53 | willUpdate(changedProperties: PropertyValues) { 54 | if ( 55 | changedProperties.has('selectedProfile') && 56 | this.selectedProfile !== null 57 | ) { 58 | // Update the paper information 59 | this.updatePaperInfo().then( 60 | () => {}, 61 | () => {} 62 | ); 63 | } 64 | } 65 | 66 | //==========================================================================|| 67 | // Custom Methods || 68 | //==========================================================================|| 69 | async updatePaperInfo() { 70 | if (this.selectedProfile === null) { 71 | console.error('Trying to update paper info when selectedProfile is null'); 72 | return; 73 | } 74 | 75 | this.isCompleted = false; 76 | 77 | let done = false; 78 | let retry = 3; 79 | 80 | let papers: SemanticPaper[] = []; 81 | 82 | while (!done && retry > 0) { 83 | try { 84 | papers = await getAllPapersFromAuthor(this.selectedProfile.authorId); 85 | done = true; 86 | } catch (e) { 87 | await new Promise(resolve => { 88 | setTimeout(() => { 89 | resolve(); 90 | }, 1500); 91 | }); 92 | retry -= 1; 93 | } 94 | } 95 | 96 | // Sort the papers by publication date first 97 | // papers.sort((a, b) => comparePaperDate(b, a)); 98 | // this.lastClickedHeader = 'year'; 99 | 100 | // Sort the papers by citation count first 101 | papers.sort((a, b) => b.citationCount - a.citationCount); 102 | this.lastClickedHeader = 'citedBy'; 103 | 104 | this.papers = papers; 105 | 106 | // Select all papers by default 107 | const newSelectedPaperIDs = new Set([ 108 | ...papers.map(d => d.paperId) 109 | ]); 110 | this.selectedPaperIDs = newSelectedPaperIDs; 111 | 112 | this.notifyParentSelectedPapers(this.selectedPaperIDs); 113 | this.notifyParentPapers(this.papers); 114 | 115 | this.isCompleted = true; 116 | // console.log(this.papers); 117 | } 118 | 119 | formatPaperAuthor(paper: SemanticPaper) { 120 | const authors = paper.authors.map(d => d.name).join(', '); 121 | return authors; 122 | } 123 | 124 | //==========================================================================|| 125 | // Event Handlers || 126 | //==========================================================================|| 127 | paperCheckboxChanged(e: InputEvent, paperID: string) { 128 | const checkboxElement = e.currentTarget as HTMLInputElement; 129 | const newSelectedPaperIDs = structuredClone(this.selectedPaperIDs); 130 | if (checkboxElement.checked) { 131 | newSelectedPaperIDs.add(paperID); 132 | } else { 133 | newSelectedPaperIDs.delete(paperID); 134 | } 135 | 136 | this.selectedPaperIDs = newSelectedPaperIDs; 137 | this.notifyParentSelectedPapers(this.selectedPaperIDs); 138 | } 139 | 140 | selectAllCheckboxChanged(e: InputEvent) { 141 | const checkboxElement = e.currentTarget as HTMLInputElement; 142 | if (checkboxElement.checked) { 143 | const newSelectedPaperIDs = new Set([ 144 | ...this.papers.map(d => d.paperId) 145 | ]); 146 | this.selectedPaperIDs = newSelectedPaperIDs; 147 | } else { 148 | const newSelectedPaperIDs = new Set(); 149 | this.selectedPaperIDs = newSelectedPaperIDs; 150 | } 151 | 152 | this.notifyParentSelectedPapers(this.selectedPaperIDs); 153 | } 154 | 155 | confirmButtonClicked() { 156 | const event = new Event('confirm-button-clicked', { 157 | bubbles: true, 158 | composed: true 159 | }); 160 | this.dispatchEvent(event); 161 | } 162 | 163 | //==========================================================================|| 164 | // Private Helpers || 165 | //==========================================================================|| 166 | notifyParentSelectedPapers(selectedPaperIDs: Set) { 167 | const event = new CustomEvent>('selected-paper-count-updated', { 168 | bubbles: true, 169 | composed: true, 170 | detail: selectedPaperIDs 171 | }); 172 | this.dispatchEvent(event); 173 | } 174 | 175 | notifyParentPapers(papers: SemanticPaper[]) { 176 | const event = new CustomEvent('papers-updated', { 177 | bubbles: true, 178 | composed: true, 179 | detail: papers 180 | }); 181 | this.dispatchEvent(event); 182 | } 183 | 184 | headerButtonClicked(button: 'title' | 'citedBy' | 'year') { 185 | // Sort the papers based on the clicked button 186 | const papers = this.papers.slice(); 187 | switch (button) { 188 | case 'title': { 189 | if (this.lastClickedHeader == 'title') { 190 | papers.sort((a, b) => b.title.localeCompare(a.title)); 191 | this.lastClickedHeader = ''; 192 | } else { 193 | papers.sort((a, b) => a.title.localeCompare(b.title)); 194 | this.lastClickedHeader = 'title'; 195 | } 196 | 197 | this.papers = papers; 198 | break; 199 | } 200 | 201 | case 'citedBy': { 202 | if (this.lastClickedHeader == 'citedBy') { 203 | papers.sort((a, b) => a.citationCount - b.citationCount); 204 | this.lastClickedHeader = ''; 205 | } else { 206 | papers.sort((a, b) => b.citationCount - a.citationCount); 207 | this.lastClickedHeader = 'citedBy'; 208 | } 209 | 210 | break; 211 | } 212 | 213 | case 'year': { 214 | if (this.lastClickedHeader == 'year') { 215 | papers.sort((a, b) => comparePaperDate(a, b)); 216 | this.lastClickedHeader = ''; 217 | } else { 218 | papers.sort((a, b) => comparePaperDate(b, a)); 219 | this.lastClickedHeader = 'year'; 220 | } 221 | break; 222 | } 223 | 224 | default: { 225 | console.error('Unknown sorting order clicked.'); 226 | break; 227 | } 228 | } 229 | 230 | this.papers = papers; 231 | } 232 | 233 | //==========================================================================|| 234 | // Templates and Styles || 235 | //==========================================================================|| 236 | render() { 237 | // Compile the table content 238 | let tableBody = html``; 239 | 240 | for (const [i, paper] of this.papers.entries()) { 241 | tableBody = html`${tableBody} 242 | 243 | 244 | { 249 | this.paperCheckboxChanged(e, paper.paperId); 250 | }} 251 | /> 252 | 253 | 254 | 255 | ${paper.title} 256 | ${this.formatPaperAuthor(paper)} 259 | ${paper.venue} 260 | 261 | ${paper.citationCount} 262 | ${paper.year} 263 | `; 264 | } 265 | 266 | // Compile the progress overlay 267 | const progressRing = html` 268 |
269 |
270 | Fetching paper details... 271 |
272 | `; 273 | 274 | return html` 275 |
276 | ${progressRing} 277 | 278 |
279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 301 | 309 | 317 | 323 | 324 | 325 | 326 | 327 | ${tableBody} 328 | 329 |
290 | 297 | this.selectAllCheckboxChanged(e)} 298 | /> 299 | 300 | 302 | 308 | this.headerButtonClicked('citedBy')} 312 | > 313 | 316 | this.headerButtonClicked('year')} 320 | > 321 | 322 |
330 |
331 | 332 | 341 |
342 | `; 343 | } 344 | 345 | static styles = [ 346 | css` 347 | ${unsafeCSS(componentCSS)} 348 | ` 349 | ]; 350 | } 351 | 352 | declare global { 353 | interface HTMLElementTagNameMap { 354 | 'recrec-paper-view': RecRecPaperView; 355 | } 356 | } 357 | 358 | const comparePaperDate = (a: SemanticPaper, b: SemanticPaper) => { 359 | const aDate = a.publicationDate || `${a.year || '0000'}-01-01`; 360 | const bDate = b.publicationDate || `${b.year || '0000'}-01-01`; 361 | return aDate.localeCompare(bDate); 362 | }; 363 | -------------------------------------------------------------------------------- /src/components/recommender-view/recommender-view.css: -------------------------------------------------------------------------------- 1 | .recommender-view { 2 | width: 100%; 3 | height: 100%; 4 | 5 | display: flex; 6 | flex-flow: row; 7 | justify-content: flex-start; 8 | align-items: flex-start; 9 | 10 | box-sizing: border-box; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | .svg-icon { 16 | display: inline-flex; 17 | justify-content: center; 18 | align-items: center; 19 | width: 1em; 20 | height: 1em; 21 | 22 | color: currentColor; 23 | transition: transform 80ms linear; 24 | transform-origin: center; 25 | 26 | & svg { 27 | fill: currentColor; 28 | width: 100%; 29 | height: 100%; 30 | } 31 | } 32 | 33 | .control-bar { 34 | flex: 0 1 auto; 35 | 36 | display: flex; 37 | flex-flow: column; 38 | border-right: 1px solid var(--gray-300); 39 | height: 100%; 40 | gap: 10px; 41 | 42 | --sl-color-primary-600: var(--blue-700); 43 | --sl-font-size-medium: 1rem; 44 | } 45 | 46 | .control-pop-bar { 47 | position: absolute; 48 | z-index: 1; 49 | background: white; 50 | 51 | display: flex; 52 | flex-flow: column; 53 | height: min-content; 54 | gap: 10px; 55 | 56 | --sl-color-primary-600: var(--blue-700); 57 | --sl-font-size-medium: 1rem; 58 | 59 | padding-bottom: 20px; 60 | border-right: 1px solid var(--gray-300); 61 | border-bottom: 1px solid var(--gray-300); 62 | border-bottom-right-radius: 5px; 63 | box-shadow: 64 | 0px 0px 5px hsla(0, 0%, 0%, 0.06), 65 | 0px 0px 10px hsla(0, 0%, 0%, 0.06), 66 | 0px 0px 20px hsla(0, 0%, 0%, 0.06); 67 | 68 | &[no-show] { 69 | display: none; 70 | } 71 | } 72 | 73 | .control-section { 74 | display: flex; 75 | flex-flow: column; 76 | width: 100%; 77 | align-items: flex-start; 78 | justify-content: flex-start; 79 | gap: 16px; 80 | 81 | &.control-section-slider { 82 | gap: 25px; 83 | } 84 | } 85 | 86 | .control-block { 87 | display: flex; 88 | flex-flow: column; 89 | width: 100%; 90 | align-items: flex-start; 91 | justify-content: flex-start; 92 | padding: 0px 20px; 93 | box-sizing: border-box; 94 | line-height: 1; 95 | 96 | .title { 97 | max-width: 240px; 98 | text-overflow: ellipsis; 99 | overflow: hidden; 100 | white-space: nowrap; 101 | display: inline-block; 102 | font-size: var(--font-u2); 103 | font-weight: 600; 104 | padding-top: 20px; 105 | line-height: 1; 106 | } 107 | 108 | nightjar-slider { 109 | width: 100%; 110 | } 111 | 112 | &.slider-block { 113 | gap: 10px; 114 | 115 | &:last-child { 116 | padding-bottom: 10px; 117 | } 118 | } 119 | 120 | .checkbox-wrapper { 121 | display: flex; 122 | align-items: center; 123 | gap: 3px; 124 | accent-color: var(--blue-700); 125 | } 126 | 127 | &.select-block { 128 | flex-flow: row; 129 | gap: 5px; 130 | align-items: center; 131 | } 132 | 133 | .select-wrapper { 134 | position: relative; 135 | 136 | &::after { 137 | right: 8px; 138 | border-right: 2px solid hsl(0, 0%, 60%); 139 | border-top: 2px solid hsl(0, 0%, 60%); 140 | content: ''; 141 | display: block; 142 | height: 5px; 143 | width: 5px; 144 | pointer-events: none; 145 | position: absolute; 146 | top: 50%; 147 | transform: translateY(-50%) rotate(135deg); 148 | transform-origin: center; 149 | transition: border 100ms linear; 150 | } 151 | 152 | &:has(.select-sort:hover) { 153 | &::after { 154 | border-right: 2px solid hsl(0, 0%, 50%); 155 | border-top: 2px solid hsl(0, 0%, 50%); 156 | } 157 | } 158 | } 159 | 160 | & select.select-sort { 161 | padding: 3px 22px 3px 5px; 162 | margin: 0px; 163 | position: relative; 164 | -moz-appearance: none; 165 | -webkit-appearance: none; 166 | 167 | border-radius: 4px; 168 | border: 1px solid var(--gray-300); 169 | background: white; 170 | 171 | font-family: inherit; 172 | font-size: var(--font-d1); 173 | color: inherit; 174 | 175 | transition: border 100ms linear; 176 | 177 | &:hover { 178 | border: 1px solid var(--gray-400); 179 | } 180 | } 181 | } 182 | 183 | .separator { 184 | height: 1px; 185 | background: var(--gray-200); 186 | width: 100%; 187 | margin: 7px 0; 188 | box-sizing: border-box; 189 | } 190 | 191 | .right-content { 192 | position: relative; 193 | flex: 1; 194 | box-sizing: border-box; 195 | padding: var(--container-v-padding) var(--container-h-padding); 196 | overflow-y: auto; 197 | height: 100%; 198 | 199 | .footer { 200 | margin-top: 20px; 201 | width: 100%; 202 | display: flex; 203 | align-items: center; 204 | justify-content: center; 205 | } 206 | } 207 | 208 | .recommender-content { 209 | display: flex; 210 | flex-flow: row wrap; 211 | justify-content: flex-start; 212 | align-items: stretch; 213 | gap: 14px; 214 | } 215 | 216 | .recommender-empty-placeholder { 217 | position: absolute; 218 | left: 0px; 219 | top: 0px; 220 | width: 100%; 221 | height: 100%; 222 | display: flex; 223 | flex-flow: column; 224 | align-items: center; 225 | justify-content: center; 226 | box-sizing: border-box; 227 | text-align: center; 228 | 229 | &[no-show] { 230 | display: none; 231 | } 232 | 233 | .text-icon { 234 | font-size: 50px; 235 | font-weight: 600; 236 | color: var(--gray-400); 237 | } 238 | 239 | .title { 240 | font-size: 20px; 241 | font-weight: 500; 242 | padding: 10px 0 5px 0; 243 | } 244 | 245 | .description { 246 | max-width: 60ch; 247 | color: var(--gray-600); 248 | } 249 | } 250 | 251 | .recommender-card { 252 | max-width: 200px; 253 | box-sizing: border-box; 254 | 255 | padding: 7px 10px; 256 | border-radius: 5px; 257 | border: 1px solid var(--gray-300); 258 | line-height: 1; 259 | cursor: default; 260 | 261 | display: flex; 262 | flex-flow: column; 263 | gap: 5px; 264 | 265 | .header { 266 | font-weight: 600; 267 | font-size: var(--font-d1); 268 | 269 | color: inherit; 270 | text-decoration: none; 271 | 272 | &:hover { 273 | color: var(--blue-800); 274 | } 275 | } 276 | 277 | .info-bar { 278 | font-size: var(--font-d4); 279 | display: flex; 280 | gap: 10px; 281 | 282 | &.icons { 283 | padding-top: 0px; 284 | margin-top: auto; 285 | } 286 | 287 | &[no-show] { 288 | display: none; 289 | } 290 | } 291 | 292 | .info-label { 293 | display: inline; 294 | max-width: 200px; 295 | white-space: nowrap; 296 | overflow: hidden; 297 | text-overflow: ellipsis; 298 | padding-bottom: 2px; 299 | } 300 | 301 | .info-award { 302 | color: var(--orange-800); 303 | white-space: wrap; 304 | } 305 | 306 | .info-block { 307 | display: flex; 308 | align-items: center; 309 | gap: 1px; 310 | background-color: var(--gray-100); 311 | padding: 3px 4px; 312 | border-radius: 2px; 313 | 314 | color: inherit; 315 | text-decoration: none; 316 | cursor: pointer; 317 | 318 | &.cite-time { 319 | gap: 2px; 320 | } 321 | 322 | &.paper-count { 323 | gap: 2px; 324 | .svg-icon { 325 | width: 0.8em; 326 | height: 0.8em; 327 | } 328 | } 329 | 330 | &:hover { 331 | background-color: var(--gray-200); 332 | } 333 | } 334 | 335 | .svg-icon { 336 | color: var(--gray-500); 337 | } 338 | } 339 | 340 | .progress-overlay { 341 | width: 100%; 342 | height: 100%; 343 | position: absolute; 344 | z-index: 5; 345 | background: white; 346 | border-radius: 10px; 347 | 348 | display: flex; 349 | flex-flow: column; 350 | align-items: center; 351 | justify-content: center; 352 | gap: 10px; 353 | transition: opacity 300ms ease-in-out; 354 | 355 | --track-width: 5px; 356 | --sl-color-primary-600: var(--blue-700); 357 | 358 | &[is-completed] { 359 | opacity: 0; 360 | pointer-events: none; 361 | } 362 | 363 | .progress-message { 364 | position: relative; 365 | 366 | &::after { 367 | display: inline; 368 | position: absolute; 369 | animation: dot-animation steps(1, end) 2000ms infinite; 370 | content: ''; 371 | } 372 | } 373 | 374 | .progress-remain-time { 375 | font-style: italic; 376 | color: var(--gray-500); 377 | margin-top: -6px; 378 | } 379 | } 380 | 381 | @keyframes dot-animation { 382 | 0% { 383 | content: ''; 384 | } 385 | 25% { 386 | content: '.'; 387 | } 388 | 50% { 389 | content: '..'; 390 | } 391 | 75% { 392 | content: '...'; 393 | } 394 | 100% { 395 | content: ''; 396 | } 397 | } 398 | 399 | button { 400 | all: unset; 401 | } 402 | 403 | button { 404 | border-radius: 5px; 405 | border: 1px solid var(--blue-600); 406 | background: var(--blue-600); 407 | color: white; 408 | font-weight: 500; 409 | 410 | padding: 3px 10px; 411 | box-sizing: border-box; 412 | position: relative; 413 | cursor: pointer; 414 | 415 | transition: 416 | background linear 100ms, 417 | border linear 100ms; 418 | 419 | &:disabled { 420 | cursor: no-drop; 421 | border: 1px solid var(--gray-300); 422 | color: var(--gray-600); 423 | background: var(--gray-100); 424 | } 425 | 426 | &:not(:disabled) { 427 | &:hover { 428 | border: 1px solid color-mix(in lab, var(--blue-600), white 10%); 429 | background-color: color-mix(in lab, var(--blue-600), white 10%); 430 | } 431 | 432 | &:active { 433 | background: var(--blue-600); 434 | border: 1px solid var(--blue-600); 435 | } 436 | } 437 | 438 | &[no-show] { 439 | display: none; 440 | } 441 | } 442 | 443 | .popper-tooltip { 444 | position: absolute; 445 | width: max-content; 446 | left: 0px; 447 | top: 0px; 448 | z-index: 20; 449 | background: var(--gray-800); 450 | color: white; 451 | box-shadow: 452 | 0 0 1px hsla(0, 0%, 0%, 0.6), 453 | 0 0 3px hsla(0, 0%, 0%, 0.05); 454 | padding: 0px 5px 3px; 455 | border-radius: 4px; 456 | font-size: var(--font-d3); 457 | 458 | display: flex; 459 | justify-content: center; 460 | box-sizing: border-box; 461 | 462 | opacity: 1; 463 | transform: scale(1); 464 | transform-origin: right center; 465 | transition: 466 | opacity 150ms linear, 467 | transform 150ms linear; 468 | 469 | &#description-overlay { 470 | transition: 471 | opacity 100ms linear, 472 | transform 100ms linear; 473 | } 474 | 475 | &[placement='right'] { 476 | transform-origin: left center; 477 | } 478 | 479 | &[placement='bottom'] { 480 | transform-origin: top center; 481 | } 482 | 483 | &[placement='top'] { 484 | transform-origin: bottom center; 485 | } 486 | 487 | &.hidden { 488 | opacity: 0; 489 | pointer-events: none; 490 | transform: scale(0.8); 491 | } 492 | 493 | &.no-show { 494 | display: none; 495 | } 496 | 497 | .popper-content { 498 | max-width: 300px; 499 | max-height: 200px; 500 | line-height: 1.5; 501 | 502 | padding: 2px 0; 503 | box-sizing: border-box; 504 | 505 | display: flex; 506 | flex-flow: column; 507 | } 508 | 509 | .description { 510 | display: flex; 511 | flex-direction: row; 512 | align-items: center; 513 | gap: 5px; 514 | 515 | .external-icon { 516 | margin-top: 2px; 517 | width: 0.7em; 518 | height: 0.7em; 519 | color: var(--gray-400); 520 | } 521 | } 522 | 523 | .table-title { 524 | padding: 0 2px; 525 | display: flex; 526 | justify-content: space-between; 527 | box-sizing: border-box; 528 | cursor: default; 529 | } 530 | 531 | .separator { 532 | width: 100%; 533 | background: var(--gray-600); 534 | height: 1px; 535 | margin: 3px 0; 536 | } 537 | 538 | .paper-table { 539 | width: 100%; 540 | flex: 1; 541 | padding: 0 2px; 542 | box-sizing: border-box; 543 | 544 | overflow-x: hidden; 545 | overflow-y: auto; 546 | 547 | display: grid; 548 | grid-template-columns: auto min-content; 549 | column-gap: 3px; 550 | row-gap: 2px; 551 | } 552 | 553 | .popper-arrow { 554 | position: absolute; 555 | background: var(--gray-800); 556 | width: 8px; 557 | height: 8px; 558 | transform: rotate(45deg); 559 | opacity: 1; 560 | 561 | &.hidden { 562 | opacity: 0; 563 | } 564 | } 565 | 566 | a.cell-paper { 567 | color: currentColor; 568 | text-decoration: none; 569 | 570 | &:hover { 571 | color: color-mix(in lab, currentColor 100%, var(--gray-600) 30%); 572 | } 573 | } 574 | 575 | .cell-paper { 576 | overflow: hidden; 577 | text-overflow: ellipsis; 578 | white-space: nowrap; 579 | } 580 | 581 | .cell-count { 582 | text-align: right; 583 | cursor: default; 584 | } 585 | } 586 | 587 | @media only screen and (max-width: 768px) { 588 | .popper-tooltip { 589 | .popper-content { 590 | max-width: 100%; 591 | table-layout: fixed; 592 | } 593 | 594 | .td-paper { 595 | max-width: unset; 596 | } 597 | 598 | .col-paper { 599 | width: auto; 600 | } 601 | 602 | .col-count { 603 | width: 20px; 604 | } 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16px; 3 | -moz-osx-font-smoothing: grayscale; 4 | -webkit-font-smoothing: antialiased; 5 | text-rendering: optimizeLegibility; 6 | -webkit-text-size-adjust: 100%; 7 | -moz-text-size-adjust: 100%; 8 | scroll-behavior: smooth; 9 | overscroll-behavior: none; 10 | overflow-x: hidden; 11 | } 12 | 13 | html, 14 | body { 15 | position: relative; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | body { 21 | margin: 0px; 22 | padding: 0px; 23 | box-sizing: border-box; 24 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 25 | Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 26 | color: hsl(0, 0%, 29%); 27 | font-size: 1em; 28 | font-weight: 400; 29 | line-height: 1.5; 30 | } 31 | 32 | *, 33 | ::after, 34 | ::before { 35 | box-sizing: border-box; 36 | } 37 | 38 | a { 39 | color: rgb(0, 100, 200); 40 | text-decoration: none; 41 | } 42 | 43 | a:hover { 44 | text-decoration: underline; 45 | } 46 | 47 | a:visited { 48 | color: rgb(0, 80, 160); 49 | } 50 | 51 | label { 52 | display: block; 53 | } 54 | 55 | input, 56 | button, 57 | select, 58 | textarea { 59 | font-family: inherit; 60 | font-size: inherit; 61 | border: 1px solid #ccc; 62 | border-radius: 2px; 63 | } 64 | 65 | input:disabled { 66 | color: #ccc; 67 | } 68 | 69 | button { 70 | color: #333; 71 | background-color: #f4f4f4; 72 | outline: none; 73 | } 74 | 75 | button:disabled { 76 | color: #999; 77 | } 78 | 79 | button:not(:disabled):active { 80 | background-color: #ddd; 81 | } 82 | 83 | button:focus { 84 | border-color: #666; 85 | } 86 | 87 | :root { 88 | /* Shadows */ 89 | --shadow-border-light: 0px 0px 5px hsla(0, 0%, 0%, 0.1), 90 | 0px 0px 4px hsla(0, 0%, 0%, 0.07), 0px 0px 10px hsla(0, 0%, 0%, 0.07); 91 | --shadow-border-card: 0px 0px 6px hsla(0, 0%, 0%, 0.07); 92 | --shadow-border-large: 0 8px 24px hsla(212, 9%, 59%, 0.2); 93 | 94 | /* Ease functions */ 95 | --ease-cubic-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); 96 | 97 | /* Layouts */ 98 | --border-radius: 5px; 99 | --container-h-padding: 20px; 100 | --container-v-padding: 20px; 101 | 102 | /* Font sizes */ 103 | --font-d1: 0.9375rem; 104 | --font-d2: 0.875rem; 105 | --font-d3: 0.8125rem; 106 | --font-d4: 0.75rem; 107 | --font-d5: 0.6875rem; 108 | --font-d6: 0.625rem; 109 | --font-d7: 0.5rem; 110 | 111 | --font-u1: 1.0625rem; 112 | --font-u2: 1.125rem; 113 | --font-u3: 1.1875rem; 114 | --font-u4: 1.25rem; 115 | --font-u5: 1.3125rem; 116 | --font-u6: 1.375rem; 117 | --font-u7: 1.5rem; 118 | 119 | /* Custom colors */ 120 | --added-color: hsl(120, 37%, 89%); 121 | --replaced-color: hsl(208, 88%, 90%); 122 | --deleted--color: hsl(353, 100%, 93%); 123 | 124 | /* Colors */ 125 | --red-50: hsl(350, 100%, 96.08%); 126 | --red-100: hsl(354, 100%, 90.2%); 127 | --red-200: hsl(0, 72.65%, 77.06%); 128 | --red-300: hsl(0, 68.67%, 67.45%); 129 | --red-400: hsl(1, 83.25%, 62.55%); 130 | --red-500: hsl(4, 89.62%, 58.43%); 131 | --red-600: hsl(1, 77.19%, 55.29%); 132 | --red-700: hsl(0, 65.08%, 50.59%); 133 | --red-800: hsl(0, 66.39%, 46.67%); 134 | --red-900: hsl(0, 73.46%, 41.37%); 135 | --red-a100: hsl(4, 100%, 75.1%); 136 | --red-a200: hsl(0, 100%, 66.08%); 137 | --red-a400: hsl(348, 100%, 54.51%); 138 | --red-a700: hsl(0, 100%, 41.76%); 139 | 140 | --pink-50: hsl(340, 80%, 94.12%); 141 | --pink-100: hsl(339, 81.33%, 85.29%); 142 | --pink-200: hsl(339, 82.11%, 75.88%); 143 | --pink-300: hsl(339, 82.56%, 66.27%); 144 | --pink-400: hsl(339, 81.9%, 58.82%); 145 | --pink-500: hsl(339, 82.19%, 51.57%); 146 | --pink-600: hsl(338, 77.78%, 47.65%); 147 | --pink-700: hsl(336, 77.98%, 42.75%); 148 | --pink-800: hsl(333, 79.27%, 37.84%); 149 | --pink-900: hsl(328, 81.33%, 29.41%); 150 | --pink-a100: hsl(339, 100%, 75.1%); 151 | --pink-a200: hsl(339, 100%, 62.55%); 152 | --pink-a400: hsl(338, 100%, 48.04%); 153 | --pink-a700: hsl(333, 84.11%, 41.96%); 154 | 155 | --purple-50: hsl(292, 44.44%, 92.94%); 156 | --purple-100: hsl(291, 46.07%, 82.55%); 157 | --purple-200: hsl(291, 46.94%, 71.18%); 158 | --purple-300: hsl(291, 46.6%, 59.61%); 159 | --purple-400: hsl(291, 46.61%, 50.78%); 160 | --purple-500: hsl(291, 63.72%, 42.16%); 161 | --purple-600: hsl(287, 65.05%, 40.39%); 162 | --purple-700: hsl(282, 67.88%, 37.84%); 163 | --purple-800: hsl(277, 70.17%, 35.49%); 164 | --purple-900: hsl(267, 75%, 31.37%); 165 | --purple-a100: hsl(291, 95.38%, 74.51%); 166 | --purple-a200: hsl(291, 95.9%, 61.76%); 167 | --purple-a400: hsl(291, 100%, 48.82%); 168 | --purple-a700: hsl(280, 100%, 50%); 169 | 170 | --deep-purple-50: hsl(264, 45.45%, 93.53%); 171 | --deep-purple-100: hsl(261, 45.68%, 84.12%); 172 | --deep-purple-200: hsl(261, 46.27%, 73.73%); 173 | --deep-purple-300: hsl(261, 46.81%, 63.14%); 174 | --deep-purple-400: hsl(261, 46.72%, 55.1%); 175 | --deep-purple-500: hsl(261, 51.87%, 47.25%); 176 | --deep-purple-600: hsl(259, 53.91%, 45.1%); 177 | --deep-purple-700: hsl(257, 57.75%, 41.76%); 178 | --deep-purple-800: hsl(254, 60.8%, 39.02%); 179 | --deep-purple-900: hsl(251, 68.79%, 33.92%); 180 | --deep-purple-a100: hsl(261, 100%, 76.67%); 181 | --deep-purple-a200: hsl(255, 100%, 65.1%); 182 | --deep-purple-a400: hsl(258, 100%, 56.08%); 183 | --deep-purple-a700: hsl(265, 100%, 45.88%); 184 | 185 | --indigo-50: hsl(231, 43.75%, 93.73%); 186 | --indigo-100: hsl(231, 45%, 84.31%); 187 | --indigo-200: hsl(230, 44.36%, 73.92%); 188 | --indigo-300: hsl(230, 44.09%, 63.53%); 189 | --indigo-400: hsl(230, 44.25%, 55.69%); 190 | --indigo-500: hsl(230, 48.36%, 47.84%); 191 | --indigo-600: hsl(231, 50%, 44.71%); 192 | --indigo-700: hsl(231, 53.62%, 40.59%); 193 | --indigo-800: hsl(232, 57.22%, 36.67%); 194 | --indigo-900: hsl(234, 65.79%, 29.8%); 195 | --indigo-a100: hsl(230, 100%, 77.45%); 196 | --indigo-a200: hsl(230, 98.84%, 66.08%); 197 | --indigo-a400: hsl(230, 98.97%, 61.76%); 198 | --indigo-a700: hsl(230, 99.04%, 59.22%); 199 | 200 | --blue-50: hsl(205, 86.67%, 94.12%); 201 | --blue-100: hsl(207, 88.89%, 85.88%); 202 | --blue-200: hsl(206, 89.74%, 77.06%); 203 | --blue-300: hsl(206, 89.02%, 67.84%); 204 | --blue-400: hsl(206, 89.95%, 60.98%); 205 | --blue-500: hsl(206, 89.74%, 54.12%); 206 | --blue-600: hsl(208, 79.28%, 50.78%); 207 | --blue-700: hsl(209, 78.72%, 46.08%); 208 | --blue-800: hsl(211, 80.28%, 41.76%); 209 | --blue-900: hsl(216, 85.06%, 34.12%); 210 | --blue-a100: hsl(217, 100%, 75.49%); 211 | --blue-a200: hsl(217, 100%, 63.33%); 212 | --blue-a400: hsl(217, 100%, 58.04%); 213 | --blue-a700: hsl(224, 100%, 58.04%); 214 | 215 | --light-blue-50: hsl(198, 93.55%, 93.92%); 216 | --light-blue-100: hsl(198, 92.41%, 84.51%); 217 | --light-blue-200: hsl(198, 92.37%, 74.31%); 218 | --light-blue-300: hsl(198, 91.3%, 63.92%); 219 | --light-blue-400: hsl(198, 91.93%, 56.27%); 220 | --light-blue-500: hsl(198, 97.57%, 48.43%); 221 | --light-blue-600: hsl(199, 97.41%, 45.49%); 222 | --light-blue-700: hsl(201, 98.1%, 41.37%); 223 | --light-blue-800: hsl(202, 97.91%, 37.45%); 224 | --light-blue-900: hsl(206, 98.72%, 30.59%); 225 | --light-blue-a100: hsl(198, 100%, 75.1%); 226 | --light-blue-a200: hsl(198, 100%, 62.55%); 227 | --light-blue-a400: hsl(198, 100%, 50%); 228 | --light-blue-a700: hsl(202, 100%, 45.88%); 229 | 230 | --cyan-50: hsl(186, 72.22%, 92.94%); 231 | --cyan-100: hsl(186, 71.11%, 82.35%); 232 | --cyan-200: hsl(186, 71.62%, 70.98%); 233 | --cyan-300: hsl(186, 71.15%, 59.22%); 234 | --cyan-400: hsl(186, 70.87%, 50.2%); 235 | --cyan-500: hsl(186, 100%, 41.57%); 236 | --cyan-600: hsl(186, 100%, 37.84%); 237 | --cyan-700: hsl(185, 100%, 32.75%); 238 | --cyan-800: hsl(185, 100%, 28.04%); 239 | --cyan-900: hsl(182, 100%, 19.61%); 240 | --cyan-a100: hsl(180, 100%, 75.88%); 241 | --cyan-a200: hsl(180, 100%, 54.71%); 242 | --cyan-a400: hsl(186, 100%, 50%); 243 | --cyan-a700: hsl(187, 100%, 41.57%); 244 | 245 | --teal-50: hsl(176, 40.91%, 91.37%); 246 | --teal-100: hsl(174, 41.28%, 78.63%); 247 | --teal-200: hsl(174, 41.9%, 64.9%); 248 | --teal-300: hsl(174, 41.83%, 50.78%); 249 | --teal-400: hsl(174, 62.75%, 40%); 250 | --teal-500: hsl(174, 100%, 29.41%); 251 | --teal-600: hsl(173, 100%, 26.86%); 252 | --teal-700: hsl(173, 100%, 23.73%); 253 | --teal-800: hsl(172, 100%, 20.59%); 254 | --teal-900: hsl(169, 100%, 15.1%); 255 | --teal-a100: hsl(166, 100%, 82.75%); 256 | --teal-a200: hsl(165, 100%, 69.61%); 257 | --teal-a400: hsl(165, 82.26%, 51.37%); 258 | --teal-a700: hsl(171, 100%, 37.45%); 259 | 260 | --green-50: hsl(124, 39.39%, 93.53%); 261 | --green-100: hsl(121, 37.5%, 84.31%); 262 | --green-200: hsl(122, 37.4%, 74.31%); 263 | --green-300: hsl(122, 38.46%, 64.31%); 264 | --green-400: hsl(122, 38.46%, 56.67%); 265 | --green-500: hsl(122, 39.44%, 49.22%); 266 | --green-600: hsl(122, 40.97%, 44.51%); 267 | --green-700: hsl(122, 43.43%, 38.82%); 268 | --green-800: hsl(123, 46.2%, 33.53%); 269 | --green-900: hsl(124, 55.37%, 23.73%); 270 | --green-a100: hsl(136, 77.22%, 84.51%); 271 | --green-a200: hsl(150, 81.82%, 67.65%); 272 | --green-a400: hsl(150, 100%, 45.1%); 273 | --green-a700: hsl(144, 100%, 39.22%); 274 | 275 | --light-green-50: hsl(88, 51.72%, 94.31%); 276 | --light-green-100: hsl(87, 50.68%, 85.69%); 277 | --light-green-200: hsl(88, 50%, 76.47%); 278 | --light-green-300: hsl(87, 50%, 67.06%); 279 | --light-green-400: hsl(87, 50.24%, 59.8%); 280 | --light-green-500: hsl(87, 50.21%, 52.75%); 281 | --light-green-600: hsl(89, 46.12%, 48.04%); 282 | --light-green-700: hsl(92, 47.91%, 42.16%); 283 | --light-green-800: hsl(95, 49.46%, 36.47%); 284 | --light-green-900: hsl(103, 55.56%, 26.47%); 285 | --light-green-a100: hsl(87, 100%, 78.24%); 286 | --light-green-a200: hsl(87, 100%, 67.45%); 287 | --light-green-a400: hsl(92, 100%, 50.59%); 288 | --light-green-a700: hsl(96, 81.15%, 47.84%); 289 | 290 | --lime-50: hsl(65, 71.43%, 94.51%); 291 | --lime-100: hsl(64, 69.01%, 86.08%); 292 | --lime-200: hsl(65, 70.69%, 77.25%); 293 | --lime-300: hsl(65, 70.37%, 68.24%); 294 | --lime-400: hsl(65, 69.7%, 61.18%); 295 | --lime-500: hsl(65, 69.96%, 54.31%); 296 | --lime-600: hsl(63, 59.68%, 49.61%); 297 | --lime-700: hsl(62, 61.43%, 43.73%); 298 | --lime-800: hsl(59, 62.89%, 38.04%); 299 | --lime-900: hsl(53, 69.93%, 30%); 300 | --lime-a100: hsl(65, 100%, 75.29%); 301 | --lime-a200: hsl(65, 100%, 62.75%); 302 | --lime-a400: hsl(73, 100%, 50%); 303 | --lime-a700: hsl(75, 100%, 45.88%); 304 | 305 | --yellow-50: hsl(55, 100%, 95.29%); 306 | --yellow-100: hsl(53, 100%, 88.43%); 307 | --yellow-200: hsl(53, 100%, 80.78%); 308 | --yellow-300: hsl(53, 100%, 73.14%); 309 | --yellow-400: hsl(53, 100%, 67.25%); 310 | --yellow-500: hsl(53, 100%, 61.57%); 311 | --yellow-600: hsl(48, 98.04%, 60%); 312 | --yellow-700: hsl(42, 96.26%, 58.04%); 313 | --yellow-800: hsl(37, 94.64%, 56.08%); 314 | --yellow-900: hsl(28, 91.74%, 52.55%); 315 | --yellow-a100: hsl(60, 100%, 77.65%); 316 | --yellow-a200: hsl(60, 100%, 50%); 317 | --yellow-a400: hsl(55, 100%, 50%); 318 | --yellow-a700: hsl(50, 100%, 50%); 319 | 320 | --amber-50: hsl(46, 100%, 94.12%); 321 | --amber-100: hsl(45, 100%, 85.1%); 322 | --amber-200: hsl(45, 100%, 75.49%); 323 | --amber-300: hsl(45, 100%, 65.49%); 324 | --amber-400: hsl(45, 100%, 57.84%); 325 | --amber-500: hsl(45, 100%, 51.37%); 326 | --amber-600: hsl(42, 100%, 50%); 327 | --amber-700: hsl(37, 100%, 50%); 328 | --amber-800: hsl(33, 100%, 50%); 329 | --amber-900: hsl(26, 100%, 50%); 330 | --amber-a100: hsl(47, 100%, 74.9%); 331 | --amber-a200: hsl(47, 100%, 62.55%); 332 | --amber-a400: hsl(46, 100%, 50%); 333 | --amber-a700: hsl(40, 100%, 50%); 334 | 335 | --orange-50: hsl(36, 100%, 93.92%); 336 | --orange-100: hsl(35, 100%, 84.9%); 337 | --orange-200: hsl(35, 100%, 75.1%); 338 | --orange-300: hsl(35, 100%, 65.1%); 339 | --orange-400: hsl(35, 100%, 57.45%); 340 | --orange-500: hsl(35, 100%, 50%); 341 | --orange-600: hsl(33, 100%, 49.22%); 342 | --orange-700: hsl(30, 100%, 48.04%); 343 | --orange-800: hsl(27, 100%, 46.86%); 344 | --orange-900: hsl(21, 100%, 45.1%); 345 | --orange-a100: hsl(38, 100%, 75.1%); 346 | --orange-a200: hsl(33, 100%, 62.55%); 347 | --orange-a400: hsl(34, 100%, 50%); 348 | --orange-a700: hsl(25, 100%, 50%); 349 | 350 | --deep-orange-50: hsl(5, 71.43%, 94.51%); 351 | --deep-orange-100: hsl(14, 100%, 86.86%); 352 | --deep-orange-200: hsl(14, 100%, 78.43%); 353 | --deep-orange-300: hsl(14, 100%, 69.8%); 354 | --deep-orange-400: hsl(14, 100%, 63.14%); 355 | --deep-orange-500: hsl(14, 100%, 56.67%); 356 | --deep-orange-600: hsl(14, 90.68%, 53.73%); 357 | --deep-orange-700: hsl(14, 80.39%, 50%); 358 | --deep-orange-800: hsl(14, 82.28%, 46.47%); 359 | --deep-orange-900: hsl(14, 88.18%, 39.8%); 360 | --deep-orange-a100: hsl(14, 100%, 75.1%); 361 | --deep-orange-a200: hsl(14, 100%, 62.55%); 362 | --deep-orange-a400: hsl(14, 100%, 50%); 363 | --deep-orange-a700: hsl(11, 100%, 43.33%); 364 | 365 | --brown-50: hsl(19, 15.79%, 92.55%); 366 | --brown-100: hsl(16, 15.79%, 81.37%); 367 | --brown-200: hsl(14, 15.19%, 69.02%); 368 | --brown-300: hsl(15, 15.32%, 56.47%); 369 | --brown-400: hsl(15, 17.5%, 47.06%); 370 | --brown-500: hsl(15, 25.39%, 37.84%); 371 | --brown-600: hsl(15, 25.29%, 34.12%); 372 | --brown-700: hsl(14, 25.68%, 29.02%); 373 | --brown-800: hsl(11, 25.81%, 24.31%); 374 | --brown-900: hsl(8, 27.84%, 19.02%); 375 | 376 | --gray-50: hsl(0, 0%, 98.04%); 377 | --gray-100: hsl(0, 0%, 96.08%); 378 | --gray-200: hsl(0, 0%, 93.33%); 379 | --gray-300: hsl(0, 0%, 87.84%); 380 | --gray-400: hsl(0, 0%, 74.12%); 381 | --gray-500: hsl(0, 0%, 61.96%); 382 | --gray-600: hsl(0, 0%, 45.88%); 383 | --gray-700: hsl(0, 0%, 38.04%); 384 | --gray-800: hsl(0, 0%, 25.88%); 385 | --gray-900: hsl(0, 0%, 12.94%); 386 | 387 | --blue-gray-50: hsl(204, 15.15%, 93.53%); 388 | --blue-gray-100: hsl(198, 15.66%, 83.73%); 389 | --blue-gray-200: hsl(199, 15.33%, 73.14%); 390 | --blue-gray-300: hsl(199, 15.63%, 62.35%); 391 | --blue-gray-400: hsl(200, 15.38%, 54.12%); 392 | --blue-gray-500: hsl(199, 18.3%, 46.08%); 393 | --blue-gray-600: hsl(198, 18.45%, 40.39%); 394 | --blue-gray-700: hsl(199, 18.34%, 33.14%); 395 | --blue-gray-800: hsl(199, 17.91%, 26.27%); 396 | --blue-gray-900: hsl(199, 19.15%, 18.43%); 397 | --blue-gray-1000: hsl(199, 20.93%, 8.43%); 398 | } 399 | 400 | @media only screen and (max-width: 768px) { 401 | html { 402 | font-size: 14px; 403 | } 404 | 405 | :root { 406 | --container-h-padding: 14px; 407 | --container-v-padding: 14px; 408 | } 409 | } 410 | --------------------------------------------------------------------------------