├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── chat.yml │ └── feature_request.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public │ └── favicon.svg ├── src │ ├── App.vue │ ├── apis │ │ ├── bank.ts │ │ ├── file.ts │ │ ├── index.ts │ │ ├── note.ts │ │ ├── profile.ts │ │ ├── question.ts │ │ └── tool.ts │ ├── assets │ │ ├── images │ │ │ ├── empty-dark.svg │ │ │ ├── empty-light.svg │ │ │ ├── empty-question-dark.svg │ │ │ ├── empty-question-light.svg │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── welcome-dark.svg │ │ │ └── welcome-light.svg │ │ ├── main.scss │ │ ├── markdown.scss │ │ └── nprogress.scss │ ├── components │ │ ├── blocks │ │ │ ├── EmptyBlock.vue │ │ │ ├── ExamineBlock.vue │ │ │ └── QuestionBlock.vue │ │ ├── card │ │ │ └── QuestionBankCard.vue │ │ ├── dialogs │ │ │ ├── ExportDialog.vue │ │ │ ├── ImportDialog.vue │ │ │ ├── ImportQuestionBankDialog.vue │ │ │ └── UploadFileDialog.vue │ │ ├── forms │ │ │ └── UploadForm.vue │ │ ├── globals │ │ │ ├── SnackBar.vue │ │ │ └── UploadingFloatFrame.vue │ │ ├── headers │ │ │ └── NoteHeader.vue │ │ ├── icons │ │ │ ├── AzureIcon.vue │ │ │ ├── NotionIcon.vue │ │ │ ├── OpenaiIcon.vue │ │ │ └── PineconeIcon.vue │ │ └── tables │ │ │ ├── FilesTable.vue │ │ │ └── QuestionTable.vue │ ├── hooks │ │ ├── index.ts │ │ ├── share.ts │ │ ├── useFetch.ts │ │ ├── useTodayListCache.ts │ │ ├── useVersion.ts │ │ └── useWatchChange.ts │ ├── layouts │ │ ├── Default.vue │ │ ├── SideBar.vue │ │ ├── TopBar.vue │ │ └── View.vue │ ├── main.ts │ ├── plugins │ │ ├── axios.ts │ │ ├── i18n │ │ │ ├── en.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── index.ts │ │ ├── nprogress.ts │ │ ├── vuetify.ts │ │ └── webfontloader.ts │ ├── router │ │ └── index.ts │ ├── store │ │ ├── file.ts │ │ ├── index.ts │ │ ├── message.ts │ │ ├── note.ts │ │ └── profile.ts │ ├── utils │ │ ├── color-switch.ts │ │ ├── date-handler.ts │ │ ├── detect-legal-file.ts │ │ ├── download.ts │ │ ├── index.ts │ │ ├── scroll-handler.ts │ │ ├── storage-hanlder.ts │ │ └── to-markdown.ts │ ├── views │ │ ├── AddNote.vue │ │ ├── Dashboard.vue │ │ ├── Note.vue │ │ ├── Notes.vue │ │ ├── Profile.vue │ │ ├── QuestionBank.vue │ │ ├── Random.vue │ │ └── Welcome.vue │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── database ├── Dockerfile └── database.sql ├── docker-compose.yaml ├── docs ├── ROADMAP.md ├── best-practices │ ├── en-best-practices.md │ ├── processing.png │ └── zh-best-practices.md ├── contribute │ ├── en-bank.md │ └── zh-bank.md ├── en-role.md ├── logo-banner.png ├── logo-text.png ├── logo.png ├── product-full.png ├── product.png ├── question-bank │ ├── en │ │ └── programming │ │ │ ├── vue-apis │ │ │ ├── application.md │ │ │ ├── built-in-components.md │ │ │ ├── built-in-directives.md │ │ │ ├── built-in-special-attributes.md │ │ │ ├── built-in-special-elements.md │ │ │ ├── component-instance.md │ │ │ ├── composition-api-dependency-injection.md │ │ │ ├── composition-api-lifecycle.md │ │ │ ├── composition-api-setup.md │ │ │ ├── custom-renderer.md │ │ │ ├── general.md │ │ │ ├── reactivity-advanced.md │ │ │ ├── reactivity-core.md │ │ │ ├── reactivity-utilities.md │ │ │ ├── render-function.md │ │ │ ├── ssr.md │ │ │ └── utility-types.md │ │ │ └── vue-component │ │ │ ├── async.md │ │ │ ├── attrs.md │ │ │ ├── events.md │ │ │ ├── props.md │ │ │ ├── provide-inject.md │ │ │ ├── registration.md │ │ │ ├── slots.md │ │ │ └── v-model.md │ └── zh │ │ └── programming │ │ ├── hello-algo │ │ ├── array.md │ │ ├── array_representation_of_tree.md │ │ ├── backtracking_algorithm.md │ │ ├── basic_data_types.md │ │ ├── binary_search.md │ │ ├── binary_search_edge.md │ │ ├── binary_search_insertion.md │ │ ├── binary_search_recur.md │ │ ├── binary_search_tree.md │ │ ├── binary_tree.md │ │ ├── binary_tree_traversal.md │ │ ├── bubble_sort.md │ │ ├── bucket_sort.md │ │ ├── build_binary_tree_problem.md │ │ ├── build_heap.md │ │ ├── character_encoding.md │ │ ├── classification_of_data_structure.md │ │ ├── counting_sort.md │ │ ├── deque.md │ │ ├── divide_and_conquer.md │ │ ├── dp_problem_features.md │ │ ├── dp_solution_pipeline.md │ │ ├── edit_distance_problem.md │ │ ├── fractional_knapsack_problem.md │ │ ├── graph.md │ │ ├── graph_operations.md │ │ ├── graph_traversal.md │ │ ├── greedy_algorithm.md │ │ ├── hanota_problem.md │ │ ├── hash_algorithm.md │ │ ├── hash_collision.md │ │ ├── hash_map.md │ │ ├── heap.md │ │ ├── heap_sort.md │ │ ├── insertion_sort.md │ │ ├── intro_to_dynamic_programming.md │ │ ├── iteration_and_recursion.md │ │ ├── knapsack_problem.md │ │ ├── linked_list.md │ │ ├── list.md │ │ ├── max_capacity_problem.md │ │ ├── max_product_cutting_problem.md │ │ ├── merge_sort.md │ │ ├── n_queens_problem.md │ │ ├── number_encoding.md │ │ ├── performance_evaluation.md │ │ ├── permutations_problem.md │ │ ├── queue.md │ │ ├── quick_sort.md │ │ ├── radix_sort.md │ │ ├── replace_linear_by_hashing.md │ │ ├── searching_algorithm_revisited.md │ │ ├── selection_sort.md │ │ ├── sorting_algorithm.md │ │ ├── space_complexity.md │ │ ├── subset_sum_problem.md │ │ ├── summary.md │ │ ├── time_complexity.md │ │ ├── top_k.md │ │ └── unbounded_knapsack_problem.md │ │ ├── vue-apis │ │ ├── application.md │ │ ├── built-in-components.md │ │ ├── built-in-directives.md │ │ ├── built-in-special-attributes.md │ │ ├── built-in-special-elements.md │ │ ├── component-instance.md │ │ ├── composition-api-dependency-injection.md │ │ ├── composition-api-lifecycle.md │ │ ├── composition-api-setup.md │ │ ├── custom-renderer.md │ │ ├── general.md │ │ ├── reactivity-advanced.md │ │ ├── reactivity-core.md │ │ ├── reactivity-utilities.md │ │ ├── render-function.md │ │ ├── ssr.md │ │ └── utility-types.md │ │ └── vue-component │ │ ├── async.md │ │ ├── attrs.md │ │ ├── events.md │ │ ├── props.md │ │ ├── provide-inject.md │ │ ├── registration.md │ │ ├── slots.md │ │ └── v-model.md ├── screen-shot │ ├── en-export-import.png │ ├── en-question-type-answer.png │ ├── en-question-type.png │ ├── en-role.png │ ├── role-emoji-en.png │ ├── role-emoji-zh.png │ ├── zh-export-import.png │ ├── zh-question-type-answer.png │ ├── zh-question-type.png │ └── zh-role.png ├── social.png ├── templates │ ├── en-vue-props.md │ └── zh-vue-props.md ├── usecase.gif ├── zh-doc.md └── zh-role.md ├── next ├── .eslintrc.json ├── .gitignore ├── Pipfile ├── README.md ├── app │ ├── add-new │ │ └── page.tsx │ ├── api │ │ ├── file │ │ │ ├── list │ │ │ │ └── route.ts │ │ │ ├── upload │ │ │ │ └── route.ts │ │ │ └── uploading │ │ │ │ └── route.ts │ │ ├── note │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ ├── all │ │ │ │ └── route.ts │ │ │ └── create │ │ │ │ └── route.ts │ │ └── profile │ │ │ ├── init │ │ │ └── route.ts │ │ │ └── update │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── manage-notes │ │ ├── _components │ │ │ ├── add-file-dialog.tsx │ │ │ ├── delete-popover.tsx │ │ │ ├── file-manager.tsx │ │ │ ├── file-table.tsx │ │ │ ├── note-header.tsx │ │ │ ├── note-icon-popover.tsx │ │ │ └── note-table.tsx │ │ ├── _context │ │ │ └── note-context.ts │ │ └── page.tsx │ ├── note │ │ └── [id] │ │ │ ├── _components │ │ │ ├── note-header.tsx │ │ │ └── question-table.tsx │ │ │ └── page.tsx │ ├── page.tsx │ ├── profile │ │ ├── _components │ │ │ ├── anthropic-config-form.tsx │ │ │ ├── azure-config-form.tsx │ │ │ ├── openai-config-form.tsx │ │ │ └── profile-form.tsx │ │ └── page.tsx │ ├── random-pick │ │ └── page.tsx │ └── template.tsx ├── components.json ├── components │ ├── form │ │ ├── drag-upload.tsx │ │ └── upload-form.tsx │ ├── layout │ │ ├── main.tsx │ │ ├── navbar │ │ │ ├── index.tsx │ │ │ ├── menu-bar.tsx │ │ │ └── mode-toggle.tsx │ │ ├── resize-panel.tsx │ │ └── sidebar │ │ │ ├── index.tsx │ │ │ ├── logo.tsx │ │ │ └── menu-list.tsx │ ├── mdi-icon.tsx │ ├── qa-block │ │ ├── answer-block.tsx │ │ ├── doc-content.tsx │ │ ├── examine-block.tsx │ │ ├── index.tsx │ │ ├── last-record.tsx │ │ ├── markdown.scss │ │ └── question-block.tsx │ ├── share │ │ ├── header.tsx │ │ ├── load-button.tsx │ │ ├── password-input.tsx │ │ ├── question-type-switch.tsx │ │ ├── role-type-switch.tsx │ │ └── uploading-popup.tsx │ ├── theme-provider.tsx │ ├── transition-animate.tsx │ └── ui │ │ ├── aspect-ratio.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navigation-menu.tsx │ │ ├── popover.tsx │ │ ├── resizable.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── delete-all-notes.js ├── hooks │ ├── useFetchNotes.ts │ ├── useHasMouted.ts │ ├── useMarkdownit.ts │ ├── useMenu.ts │ └── useUploadingNote.ts ├── langchain │ ├── chain │ │ ├── __test__ │ │ │ └── chain.test.ts │ │ ├── index.ts │ │ └── util.ts │ ├── llm │ │ ├── __tests__ │ │ │ └── llm.test.ts │ │ ├── index.ts │ │ ├── intergration.ts │ │ └── pure.ts │ ├── loader │ │ ├── index.ts │ │ ├── markdown.ts │ │ └── share.ts │ └── prompt │ │ ├── en │ │ ├── answer-examine.ts │ │ └── question-generate.ts │ │ └── index.ts ├── lib │ ├── async-concurrent-control.ts │ ├── contants.ts │ ├── db-handler │ │ ├── document.ts │ │ ├── file.ts │ │ ├── index.ts │ │ ├── note.ts │ │ ├── profile.ts │ │ └── question.ts │ ├── ebbinghaus-memory.ts │ ├── file-handler.ts │ └── utils.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── prisma │ ├── migrations │ │ ├── 20240324085117_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── public │ └── images │ │ ├── empty-dark.svg │ │ ├── empty-light.svg │ │ ├── empty-question-dark.svg │ │ ├── empty-question-light.svg │ │ ├── logo-dark.svg │ │ ├── logo.svg │ │ ├── welcome-dark.svg │ │ └── welcome-light.svg ├── python │ └── test.py ├── schema │ ├── profile.ts │ └── upload.ts ├── store │ ├── file.ts │ ├── index.ts │ ├── note.ts │ └── profile.ts ├── tailwind.config.ts ├── tsconfig.json ├── types │ └── global.ts └── vitest.config.ts └── server ├── .dockerignore ├── Dockerfile ├── README.md ├── apis ├── __init__.py ├── bank.py ├── file.py ├── note.py ├── profile.py └── question.py ├── db_services ├── MySQLHandler.py ├── __init__.py ├── document.py ├── file.py ├── note.py ├── profile.py └── question.py ├── llm_services ├── __init__.py ├── langchain_chain.py ├── langchain_llm.py └── pure_llm.py ├── loaders ├── __init__.py ├── markdown.py └── share.py ├── main.py ├── prompts ├── __init__.py ├── cn │ ├── answer_examine.py │ └── question_generate.py └── en │ ├── answer_examine.py │ └── question_generate.py ├── question_bank ├── en │ └── programming │ │ ├── vue-apis.json │ │ └── vue-component.json └── zh │ └── programming │ ├── hello-algo.json │ ├── vue-apis.json │ └── vue-component.json ├── requirements.txt ├── utils ├── __init__.py ├── api_result.py ├── bank_handler.py ├── ebbinghaus.py ├── file_handler.py └── types.py └── wait-for-it.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | /dist 2 | profile.json 3 | node_modules 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | # Python dir 24 | __pycache__ 25 | profile.json -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛Bug Report 2 | description: File a bug report here 3 | title: "[BUG]: " 4 | labels: ["bug"] 5 | assignees: "codeacme17" 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report 🤗 11 | Make sure there aren't any open/closed issues for this topic 😃 12 | 13 | - type: textarea 14 | id: bug-description 15 | attributes: 16 | label: Description of the bug 17 | description: Give us a brief description of what happened and what should have happened 18 | validations: 19 | required: true 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chat.yml: -------------------------------------------------------------------------------- 1 | name: 💬Chat 2 | description: Just chat 3 | labels: ['chat'] 4 | assignees: 'codeacme17' 5 | title: '[CHAT]: ' 6 | body: 7 | - type: textarea 8 | id: description 9 | attributes: 10 | label: Description 11 | description: Please describe what you want to talk about here 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ✨Feature Request 2 | description: Request a new feature or enhancement 3 | labels: ["enhancement"] 4 | assignees: "codeacme17" 5 | title: "[FEAT]: " 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Please make sure this feature request hasn't been already submitted by someone by looking through other open/closed issues 😃 11 | 12 | - type: textarea 13 | id: description 14 | attributes: 15 | label: Description 16 | description: Give us a brief description of the feature or enhancement you would like 17 | validations: 18 | required: true 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # Python dir 26 | __pycache__ 27 | profile.json 28 | notebooks 29 | data.xlsx 30 | user-data.xlsx 31 | temp 32 | .venv 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to examor 2 | 3 | First off, thanks for taking the time to contribute! ❤️ 4 | 5 | The following is a set of guidelines for contributing to examor. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## How Can I Contribute? 8 | 9 | ### Reporting Bugs 10 | 11 | Bugs are tracked as GitHub issues. Explain the problem and include additional details to help maintainers reproduce the problem: 12 | 13 | - Use a clear and descriptive title for the issue to identify the problem. 14 | - Describe the exact steps which reproduce the problem. Share examples if possible. 15 | - Explain which behavior you expected to see instead and why. 16 | 17 | ### Suggesting Enhancements 18 | 19 | Enhancement suggestions are also tracked as GitHub issues. 20 | 21 | - Use a clear and descriptive title for the issue to identify the suggestion. 22 | - Provide a step-by-step description of the suggested enhancement. 23 | - Explain why this enhancement would be useful. 24 | 25 | ### Pull Requests 26 | 27 | - Fork the repo and create your branch from master. 28 | - Make sure your code lints and tests pass. 29 | - Issue pull request linking to the issue it addresses. 30 | 31 | ## Development Setup 32 | 33 | - Fork and clone the repo 34 | - Getting stuff running 35 | - app(node >= 16) 36 | ```bash 37 | cd app 38 | pnpm install 39 | pnpm dev 40 | ``` 41 | - server(python3.11) 42 | ```bash 43 | cd server 44 | pip install -r requirements.txt 45 | uvicorn main:app --reload --port 51717 --host 0.0.0.0 46 | ``` 47 | - database 48 | ```bash 49 | cd db 50 | docker build -t test-database . 51 | docker run -d -p 52020:3306 --name test-database-c test-database 52 | ``` 53 | - Create a branch for your changes 54 | - Make your changes and run tests 55 | - Commit your changes and push your branch 56 | - Submit a PR for review 57 | 58 | ## Code Style 59 | 60 | - 2 spaces for indentation 61 | - 80 character line length 62 | - Use `prettier` & `autopep8` formatting 63 | 64 | Feel free to contribute! 🎉 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Vulnerabilities 4 | 5 | If it so happens that you find a security vulnerability with the software, please immediately directly email 6 | -------------------------------------------------------------------------------- /app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /app/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /app/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | ], 11 | rules: { 12 | 'vue/multi-word-component-names': 'off', 13 | 'no-constant-condition': 'off', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json pnpm-lock.yaml ./ 6 | 7 | RUN npm install -g pnpm && \ 8 | pnpm install && \ 9 | rm -rf /node_modules/.vite 10 | 11 | COPY . . 12 | 13 | ENV DOCKER=true 14 | 15 | EXPOSE 51818 16 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### Install 6 | 7 | ```bash 8 | pnpm install 9 | ``` 10 | 11 | ### Run 12 | 13 | ```bash 14 | pnpm dev 15 | ``` 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | examor | self-improvement 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.4.2", 4 | "scripts": { 5 | "dev": "vite --host", 6 | "dev:docker": "vite --host --force", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "preview": "vite preview", 9 | "lint": "eslint . --fix --ignore-path .gitignore" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "7.0.96", 13 | "@types/nprogress": "^0.2.0", 14 | "@vuelidate/core": "^2.0.3", 15 | "@vuelidate/validators": "^2.0.3", 16 | "@vueuse/core": "^10.2.1", 17 | "axios": "^1.4.0", 18 | "highlight.js": "^11.8.0", 19 | "markdown-it": "^13.0.1", 20 | "nprogress": "^0.2.0", 21 | "pinia": "^2.0.23", 22 | "roboto-fontface": "*", 23 | "sass": "^1.63.6", 24 | "tdesign-vue-next": "^1.3.11", 25 | "vue": "^3.2.0", 26 | "vue-i18n": "^9.2.2", 27 | "vue-router": "^4.0.0", 28 | "vuetify": "^3.0.0", 29 | "webfontloader": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/types": "^7.21.4", 33 | "@types/markdown-it": "^12.2.3", 34 | "@types/node": "^18.15.0", 35 | "@types/webfontloader": "^1.6.35", 36 | "@vitejs/plugin-vue": "^4.0.0", 37 | "@vue/eslint-config-typescript": "^11.0.0", 38 | "eslint": "^8.0.0", 39 | "eslint-plugin-vue": "^9.0.0", 40 | "typescript": "^5.0.0", 41 | "unplugin-auto-import": "^0.16.6", 42 | "unplugin-vue-components": "^0.25.1", 43 | "vite": "^4.4.9", 44 | "vite-plugin-vuetify": "^1.0.0", 45 | "vue-tsc": "^1.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 69 | -------------------------------------------------------------------------------- /app/src/apis/bank.ts: -------------------------------------------------------------------------------- 1 | import _axios from '@/plugins/axios' 2 | 3 | export const BANK_API = { 4 | getCategories(language: string) { 5 | return _axios({ 6 | method: 'GET', 7 | url: `/api/bank/${language}/categories`, 8 | }) 9 | }, 10 | 11 | getBanks(data: { language: string; category: string }) { 12 | return _axios({ 13 | method: 'GET', 14 | url: `/api/bank/${data.language}/${data.category}`, 15 | }) 16 | }, 17 | 18 | importBank(data: { 19 | import_type: string 20 | note_id: number 21 | note_name: string 22 | language: string 23 | category: string 24 | bank_name: string 25 | }) { 26 | return _axios({ 27 | method: 'POST', 28 | url: `/api/bank/import`, 29 | data, 30 | }) 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /app/src/apis/file.ts: -------------------------------------------------------------------------------- 1 | import _axios from '@/plugins/axios' 2 | 3 | export const FILE_API = { 4 | getQuestionCount(id: number) { 5 | return _axios({ 6 | method: 'GET', 7 | url: `/api/file/${id}/questionCount`, 8 | }) 9 | }, 10 | 11 | deleteFile(data: any) { 12 | return _axios({ 13 | method: 'DELETE', 14 | url: `/api/file`, 15 | params: data, 16 | }) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /app/src/apis/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile' 2 | export * from './note' 3 | export * from './file' 4 | export * from './question' 5 | export * from './tool' 6 | export * from './bank' 7 | -------------------------------------------------------------------------------- /app/src/apis/note.ts: -------------------------------------------------------------------------------- 1 | import _axios from '@/plugins/axios' 2 | 3 | export const NOTE_API = { 4 | getNotes() { 5 | return _axios({ 6 | method: 'GET', 7 | url: '/api/note/notes', 8 | }) 9 | }, 10 | 11 | getNote(id: number) { 12 | return _axios({ 13 | method: 'GET', 14 | url: `/api/note/${id}`, 15 | }) 16 | }, 17 | 18 | getFiles(id: number) { 19 | return _axios({ 20 | method: 'GET', 21 | url: `/api/note/${id}/files`, 22 | }) 23 | }, 24 | 25 | getQuestions(id: number) { 26 | return _axios({ 27 | method: 'GET', 28 | url: `/api/note/${id}/questions`, 29 | }) 30 | }, 31 | 32 | addNote(data: any) { 33 | return _axios({ 34 | method: 'POST', 35 | url: `/api/note`, 36 | headers: { 37 | 'Content-Type': 'multipart/form-data', 38 | }, 39 | data: data.formData, 40 | }) 41 | }, 42 | 43 | addFile(data: any) { 44 | return _axios({ 45 | method: 'POST', 46 | url: `/api/note/${data.id}/file`, 47 | headers: { 48 | 'Content-Type': 'multipart/form-data', 49 | }, 50 | data: data.formData, 51 | timeout: 100000 * 1000, 52 | }) 53 | }, 54 | 55 | deleteNote(id: number) { 56 | return _axios({ 57 | method: 'DELETE', 58 | url: `/api/note/${id}`, 59 | }) 60 | }, 61 | 62 | updateNoteIcon(data: any) { 63 | return _axios({ 64 | method: 'PATCH', 65 | url: '/api/note/icon', 66 | data: data, 67 | }) 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /app/src/apis/profile.ts: -------------------------------------------------------------------------------- 1 | import _axios from '@/plugins/axios' 2 | 3 | export const PROFILE_API = { 4 | getProfile() { 5 | return _axios({ 6 | method: 'GET', 7 | url: '/api/profile', 8 | }) 9 | }, 10 | 11 | setProfile(data: any) { 12 | return _axios({ 13 | method: 'PUT', 14 | url: '/api/profile', 15 | data: data, 16 | }) 17 | }, 18 | 19 | checkLlmApiState() { 20 | return _axios({ 21 | method: 'GET', 22 | url: '/api/profile/auth/llm', 23 | }) 24 | }, 25 | 26 | exportData(data: { isProfile: boolean; isNotes: boolean }) { 27 | return _axios({ 28 | method: 'GET', 29 | url: '/api/profile/data', 30 | responseType: 'blob', 31 | params: { 32 | isProfile: data.isProfile, 33 | isNotes: data.isNotes, 34 | }, 35 | }) 36 | }, 37 | 38 | importData(data: FormData) { 39 | return _axios({ 40 | method: 'POST', 41 | url: `/api/profile/data`, 42 | headers: { 43 | 'Content-Type': 'multipart/form-data', 44 | }, 45 | data: data, 46 | }) 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /app/src/apis/question.ts: -------------------------------------------------------------------------------- 1 | import _axios from '@/plugins/axios' 2 | 3 | export const QUESTION_API = { 4 | getLastAnswer(id: number) { 5 | return _axios({ 6 | method: 'GET', 7 | url: `/api/question/${id}/lastAnswer`, 8 | }) 9 | }, 10 | 11 | getDocument(id: number) { 12 | return _axios({ 13 | method: 'GET', 14 | url: `/api/question/${id}/document`, 15 | }) 16 | }, 17 | 18 | getRandomQuestion() { 19 | return _axios({ 20 | method: 'GET', 21 | url: '/api/question/random', 22 | }) 23 | }, 24 | 25 | examingAnswer(data: any) { 26 | return fetch('/api/question/examine', { 27 | method: 'POST', 28 | headers: { 29 | Accept: 'application/json', 30 | 'Content-Type': 'application/json', 31 | }, 32 | body: JSON.stringify(data), 33 | }) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /app/src/apis/tool.ts: -------------------------------------------------------------------------------- 1 | import _axios from '@/plugins/axios' 2 | 3 | export const TOOL_API = { 4 | getTagVersion() { 5 | return _axios({ 6 | method: 'GET', 7 | url: 'https://api.github.com/repos/codeacme17/examor/tags', 8 | }) 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /app/src/assets/images/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/assets/main.scss: -------------------------------------------------------------------------------- 1 | @use 'vuetify' with ( 2 | $color-pack: false 3 | ); 4 | 5 | @import url('./markdown.scss'); 6 | @import 'tdesign-vue-next/es/style/index.css'; 7 | 8 | :root { 9 | --td-brand-color: #000 !important; 10 | --td-brand-color-hover: #000 !important; 11 | --list-item-font-size: 12px; 12 | --code-box-bg: #aacef3; 13 | --cood-block-bg: #ededed; 14 | --thumb-bg: #d8d8d8; 15 | --gray-bg: #bfbfbf7a; 16 | } 17 | 18 | :root[theme-mode='dark'] { 19 | --td-brand-color: #fff !important; 20 | --td-brand-color-hover: #fff !important; 21 | --code-box-bg: #0d47a1; 22 | --cood-block-bg: #282828; 23 | --thumb-bg: #424242; 24 | --gray-bg: #60606084; 25 | } 26 | 27 | html { 28 | font-family: 'Poppins', 'JetBrains Mono', '微软雅黑', '圆体-简' !important; 29 | } 30 | 31 | .main_width { 32 | max-width: 900px !important; 33 | margin: 0 auto !important; 34 | } 35 | 36 | .v-btn--disabled { 37 | opacity: 0.5 !important; 38 | } 39 | 40 | #nprogess-bar { 41 | background: linear-gradient(to right, red, yellow, blue) !important; 42 | } 43 | 44 | .footer-leave-active { 45 | transition: opacity 0.5s ease; 46 | } 47 | 48 | .footer-enter-active { 49 | transition: opacity 0.2s 0.1s ease; 50 | } 51 | 52 | .footer-enter-from, 53 | .footer-leave-to { 54 | opacity: 0; 55 | } 56 | 57 | .t-upload__flow-bottom { 58 | display: none !important; 59 | } 60 | 61 | ::-webkit-scrollbar { 62 | width: 6px; 63 | height: 3px; 64 | background-color: transparent; 65 | } 66 | ::-webkit-scrollbar-thumb { 67 | border-radius: 10px; 68 | height: 3px; 69 | background-color: var(--thumb-bg); 70 | } 71 | -------------------------------------------------------------------------------- /app/src/assets/nprogress.scss: -------------------------------------------------------------------------------- 1 | #nprogress .bar { 2 | background: var(--td-brand-color) !important; 3 | } 4 | #nprogress .spinner { 5 | display: none; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/components/blocks/EmptyBlock.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 47 | -------------------------------------------------------------------------------- /app/src/components/card/QuestionBankCard.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 70 | -------------------------------------------------------------------------------- /app/src/components/dialogs/UploadFileDialog.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /app/src/components/globals/SnackBar.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /app/src/components/globals/UploadingFloatFrame.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 49 | 50 | 71 | -------------------------------------------------------------------------------- /app/src/components/icons/AzureIcon.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /app/src/components/icons/NotionIcon.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /app/src/components/icons/OpenaiIcon.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /app/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './share' 2 | export * from './useFetch' 3 | export * from './useWatchChange' 4 | export * from './useTodayListCache' 5 | export * from './useVersion' 6 | -------------------------------------------------------------------------------- /app/src/hooks/share.ts: -------------------------------------------------------------------------------- 1 | import { Ref, computed } from 'vue' 2 | 3 | /** 4 | * Computes the disabled state of a confirm button based on form data and required fields. 5 | * 6 | * @param {Object} formData - The form data object. 7 | * @param {Object} required - An object indicating which fields are required. 8 | * @returns {Ref} A computed ref representing the disabled state of the confirm button. 9 | */ 10 | export const useConfirmBtnDisabled = ( 11 | formData: any, 12 | required: any 13 | ): Ref => { 14 | return computed(() => { 15 | for (const key in formData) { 16 | const element = formData[key] 17 | 18 | if (Array.isArray(element) && required[key]) { 19 | if (!element.length) { 20 | return true 21 | } 22 | } 23 | 24 | if (!element && required[key]) return true 25 | } 26 | 27 | return false 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/hooks/useFetch.ts: -------------------------------------------------------------------------------- 1 | import { MessagePlugin } from 'tdesign-vue-next' 2 | import { ref, Ref } from 'vue' 3 | import type { ResponseBody } from '@/plugins/axios' 4 | 5 | /** 6 | * Creates a fetch function with loading state and optional success message. 7 | * 8 | * @param {Function} fun - The asynchronous function to perform the fetch operation. 9 | * @param {string} [successMessage] - Optional message to display on fetch success. 10 | * @returns {[Function, Ref]} A tuple containing the fetch function and a ref representing the loading state. 11 | */ 12 | export function useFetch( 13 | fun: (data?: any) => Promise, 14 | successMessage?: string 15 | ): [Function, Ref] { 16 | const loading = ref(false) 17 | 18 | const fetch = async (data: any) => { 19 | loading.value = true 20 | 21 | try { 22 | const res: ResponseBody = await fun(data) 23 | if (res.code === 0 && successMessage) 24 | MessagePlugin.success(successMessage, 2000) 25 | return res 26 | } finally { 27 | loading.value = false 28 | } 29 | } 30 | 31 | return [fetch, loading] 32 | } 33 | -------------------------------------------------------------------------------- /app/src/hooks/useTodayListCache.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | import { useNow, useDateFormat, useLocalStorage } from '@vueuse/core' 3 | import { useFetch } from './useFetch' 4 | import type { ResponseBody } from '@/plugins/axios' 5 | import type { TableItem } from '@/components/tables/QuestionTable.vue' 6 | 7 | const today = useDateFormat(useNow(), 'YYYY-MM-DD') 8 | 9 | /** 10 | * If a user requests a list of issues under a note once today, 11 | * then the list is cached, and when user enter the problem list page again, 12 | * the interface is no longer called 13 | * 14 | * @param {string} noteId - The ID of the note. 15 | * @param {Function} fun - The asynchronous function to fetch data. 16 | * @returns {Promise} A promise that resolves with an array containing the cached list and loading state. 17 | */ 18 | export const useTodayListCache = async ( 19 | noteId: string, 20 | fun: (id: number) => Promise 21 | ): Promise => { 22 | const [fetch, loading] = useFetch(fun) 23 | const list = ref([]) 24 | const key = `${today.value}:${noteId}` 25 | const cachedData = 26 | localStorage.getItem(key) && JSON.parse(localStorage.getItem(key)!) 27 | if ( 28 | cachedData && 29 | (!!cachedData.expired.length || 30 | !!cachedData.today.length || 31 | !!cachedData.supplement.length) 32 | ) { 33 | list.value = cachedData 34 | } else { 35 | const { data } = await fetch(noteId) 36 | list.value = data 37 | localStorage.setItem(key, JSON.stringify(list.value)) 38 | } 39 | 40 | return [list, loading] 41 | } 42 | 43 | /** 44 | * Creates state for pending and finished lists. 45 | * 46 | * @returns {Array>>} An array containing refs for pending and finished lists. 47 | */ 48 | export const useListState = () => { 49 | const pendingList = useLocalStorage(`${today.value}:pendingList`, new Set()) 50 | const finishedList = useLocalStorage(`${today.value}:finishedList`, new Set()) 51 | return [pendingList, finishedList] 52 | } 53 | -------------------------------------------------------------------------------- /app/src/hooks/useVersion.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | import { useFetch } from './useFetch' 3 | import { TOOL_API } from '@/apis' 4 | import { useMessageStore } from '@/store' 5 | 6 | /** 7 | * Checks for updates and shows a message if an update is available. 8 | * 9 | * @param {string} message - The message to show if an update is available. 10 | * @returns {Promise>} A promise that resolves with a ref indicating whether an update is needed. 11 | */ 12 | export const useVersion = async (message: string): Promise> => { 13 | const MESSAGE_STORE = useMessageStore() 14 | 15 | const [fetch] = useFetch(TOOL_API.getTagVersion) 16 | const isNeedUpdate = ref(false) 17 | const githubRes = await fetch() 18 | const localRes = await import('../../package.json') 19 | const lastVersion = githubRes[0].name.split('v')[1] 20 | const currentVersion = localRes.version 21 | 22 | const res = compareVersions(lastVersion, currentVersion) 23 | if (res === 1) { 24 | isNeedUpdate.value = true 25 | MESSAGE_STORE.show( 26 | message, 27 | 'button', 28 | '', 29 | 'https://github.com/codeacme17/examor#%EF%B8%8F-update-the-project' 30 | ) 31 | } else isNeedUpdate.value = false 32 | 33 | return isNeedUpdate 34 | } 35 | 36 | /** 37 | * Compares two versions and returns a comparison result. 38 | * 39 | * @param {string} version1 - The first version to compare. 40 | * @param {string} version2 - The second version to compare. 41 | * @returns {number} -1 if version1 is smaller, 1 if version1 is larger, 0 if equal. 42 | */ 43 | function compareVersions(version1: string, version2: string): number { 44 | const parts1 = version1.split('.') 45 | const parts2 = version2.split('.') 46 | 47 | for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { 48 | const num1 = parseInt(parts1[i] || '0', 10) 49 | const num2 = parseInt(parts2[i] || '0', 10) 50 | 51 | if (num1 < num2) return -1 52 | else if (num1 > num2) return 1 53 | } 54 | 55 | return 0 56 | } 57 | -------------------------------------------------------------------------------- /app/src/hooks/useWatchChange.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | 3 | export const useWatchChange = (obj: any) => { 4 | const isChanged = ref(false) 5 | 6 | watch( 7 | obj, 8 | (oldValue, newValue) => { 9 | if (oldValue === newValue) isChanged.value = false 10 | else isChanged.value = true 11 | }, 12 | { 13 | deep: true, 14 | } 15 | ) 16 | 17 | return isChanged 18 | } 19 | -------------------------------------------------------------------------------- /app/src/layouts/Default.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /app/src/layouts/View.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /app/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import { createApp } from 'vue' 3 | import { registerPlugins } from '@/plugins' 4 | 5 | import '@/assets/main.scss' 6 | 7 | const app = createApp(App) 8 | registerPlugins(app) 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /app/src/plugins/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessagePlugin } from 'tdesign-vue-next' 3 | 4 | const _axios = axios.create() 5 | 6 | export type ResponseBody = { 7 | code: number 8 | message: string 9 | data: Record 10 | } 11 | 12 | _axios.interceptors.request.use( 13 | (config) => { 14 | return config 15 | }, 16 | (error) => { 17 | return Promise.reject(error) 18 | } 19 | ) 20 | 21 | _axios.interceptors.response.use( 22 | (response) => { 23 | const { headers } = response 24 | const { code, message } = response.data 25 | 26 | if (code !== 0 && headers['content-type'] === 'application/json') { 27 | MessagePlugin.error({ 28 | content: message, 29 | duration: 3000, 30 | }) 31 | } 32 | 33 | return response.data 34 | }, 35 | (error) => { 36 | MessagePlugin.error({ 37 | content: error.message, 38 | duration: 3000, 39 | }) 40 | return Promise.reject(error) 41 | } 42 | ) 43 | 44 | export default _axios 45 | -------------------------------------------------------------------------------- /app/src/plugins/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import { useStorage } from '@vueuse/core' 3 | 4 | import * as zh from './zh-CN' 5 | import * as en from './en' 6 | 7 | const message = { 8 | 'zh-CN': { 9 | ...zh.zhCN, 10 | }, 11 | en: { 12 | ...en.en, 13 | }, 14 | } 15 | 16 | const lang = useStorage('local-lang', navigator.language) 17 | 18 | const i18n = createI18n({ 19 | locale: lang.value, 20 | legacy: false, 21 | globalInjection: true, 22 | messages: message, 23 | }) 24 | 25 | export default i18n 26 | -------------------------------------------------------------------------------- /app/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { loadFonts } from './webfontloader' 2 | import vuetify from './vuetify' 3 | import i18n from './i18n' 4 | import pinia from '@/store' 5 | import router from '@/router' 6 | 7 | import type { App } from 'vue' 8 | 9 | export function registerPlugins(app: App) { 10 | loadFonts() 11 | app.use(i18n).use(vuetify).use(router).use(pinia) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/plugins/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import 'nprogress/nprogress.css' 3 | import '@/assets/nprogress.scss' 4 | 5 | NProgress.configure({ 6 | showSpinner: false, 7 | }) 8 | 9 | export default NProgress 10 | -------------------------------------------------------------------------------- /app/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import 'vuetify/styles' 3 | import { createVuetify } from 'vuetify' 4 | 5 | export default createVuetify({ 6 | theme: { 7 | themes: { 8 | light: { 9 | colors: { 10 | background: '#FFFFFF', 11 | surface: '#FFFFFF', 12 | primary: '#7d09f1', 13 | secondary: '#03DAC6', 14 | 'primary-darken-1': '#474787', 15 | 'secondary-darken-1': '#018786', 16 | error: '#B00020', 17 | info: '#2196F3', 18 | success: '#4CAF50', 19 | warning: '#FB8C00', 20 | gray: '#3f3f46', 21 | }, 22 | }, 23 | 24 | dark: { 25 | colors: { 26 | background: '#1e1e1e', 27 | surface: '#1e1e1e', 28 | primary: '#a858f9', 29 | secondary: '#03DAC6', 30 | 'primary-darken-1': '#474787', 31 | 'secondary-darken-1': '#018786', 32 | gray: '#71717a', 33 | }, 34 | }, 35 | }, 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /app/src/plugins/webfontloader.ts: -------------------------------------------------------------------------------- 1 | export async function loadFonts() { 2 | const webFontLoader = await import( 3 | /* webpackChunkName: "webfontloader" */ 'webfontloader' 4 | ) 5 | 6 | webFontLoader.load({ 7 | google: { 8 | families: [ 9 | 'Poppins:100,300,400,500,700,900&display=swap', 10 | 'JetBrains Mono', 11 | ], 12 | }, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import NProgress from '@/plugins/nprogress' 3 | 4 | const routes = [ 5 | { 6 | path: '/', 7 | component: () => import('@/layouts/Default.vue'), 8 | redirect: () => { 9 | if (!JSON.parse(localStorage.getItem('isWelcome')!)) return '/welcome' 10 | else return '/notes' 11 | }, 12 | children: [ 13 | { 14 | path: '/welcome', 15 | name: 'Welcome', 16 | component: () => import('@/views/Welcome.vue'), 17 | }, 18 | { 19 | path: '/dashboard', 20 | name: 'Dashboard', 21 | component: () => import('@/views/Dashboard.vue'), 22 | }, 23 | { 24 | path: '/notes', 25 | name: 'Notes', 26 | component: () => import('@/views/Notes.vue'), 27 | }, 28 | { 29 | path: '/random', 30 | name: 'Random', 31 | component: () => import('@/views/Random.vue'), 32 | }, 33 | { 34 | path: '/question-bank', 35 | name: 'QuesitonBank', 36 | component: () => import('@/views/QuestionBank.vue'), 37 | }, 38 | { 39 | path: '/addNote', 40 | name: 'AddNote', 41 | component: () => import('@/views/AddNote.vue'), 42 | }, 43 | { 44 | path: '/profile', 45 | name: 'Profile', 46 | component: () => import('@/views/Profile.vue'), 47 | }, 48 | { 49 | path: '/note/:id', 50 | name: 'Note', 51 | component: () => import('@/views/Note.vue'), 52 | }, 53 | ], 54 | }, 55 | ] 56 | 57 | const router = createRouter({ 58 | history: createWebHistory(process.env.BASE_URL), 59 | routes, 60 | }) 61 | 62 | router.beforeEach((a, b, next) => { 63 | NProgress.start() 64 | next() 65 | }) 66 | 67 | router.afterEach(() => { 68 | NProgress.done() 69 | NProgress.remove() 70 | }) 71 | 72 | export default router 73 | -------------------------------------------------------------------------------- /app/src/store/file.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export type UploadingFileItem = { 4 | id: number 5 | note_id: number 6 | file_name: string 7 | } 8 | 9 | type State = { 10 | uploadingFiles: UploadingFileItem[] 11 | } 12 | 13 | const state: State = { 14 | uploadingFiles: [], 15 | } 16 | 17 | export const useFileStore = defineStore('fileStore', { 18 | state: () => state, 19 | }) 20 | -------------------------------------------------------------------------------- /app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | export * from './profile' 4 | export * from './note' 5 | export * from './file' 6 | export * from './message' 7 | 8 | export default createPinia() 9 | -------------------------------------------------------------------------------- /app/src/store/message.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | type MessageType = 'default' | 'button' 4 | 5 | type State = { 6 | isShow: boolean 7 | message: string 8 | type: MessageType 9 | duration?: number 10 | path?: string 11 | url?: string 12 | } 13 | 14 | const state: State = { 15 | isShow: false, 16 | message: '', 17 | type: 'default', 18 | duration: 1500, 19 | path: '', 20 | url: '', 21 | } 22 | 23 | export const useMessageStore = defineStore('messageStore', { 24 | state: () => state, 25 | 26 | actions: { 27 | show( 28 | message: string, 29 | type: MessageType, 30 | path?: string, 31 | url?: string, 32 | duration?: number 33 | ) { 34 | this.$state.message = message 35 | this.$state.type = type 36 | this.$state.isShow = true 37 | this.$state.duration = duration 38 | this.$state.path = path 39 | this.$state.url = url 40 | }, 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /app/src/store/note.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { NOTE_API } from '@/apis' 3 | import { useFetch } from '@/hooks' 4 | import { useFileStore, type UploadingFileItem } from './file' 5 | 6 | type State = { 7 | notes: NoteItem[] 8 | currentIcon: string 9 | getNotesLoading: boolean 10 | } 11 | 12 | export type NoteItem = { 13 | id: number 14 | name: string 15 | icon: string 16 | upload_date?: string 17 | isUploading?: boolean 18 | } 19 | 20 | const state: State = { 21 | notes: [], 22 | currentIcon: '', 23 | getNotesLoading: false, 24 | } 25 | 26 | export const useNoteStore = defineStore('noteStore', { 27 | state: () => state, 28 | 29 | actions: { 30 | async getNotes() { 31 | const [_getNotes, loading] = useFetch(NOTE_API.getNotes) 32 | // @ts-ignore 33 | this.$state.getNotesLoading = loading 34 | const { data } = await _getNotes() 35 | this.$state.notes = data 36 | }, 37 | 38 | setIsUploadingNotes() { 39 | const FILE_STORE = useFileStore() 40 | const noteIdsSet = new Set( 41 | FILE_STORE.uploadingFiles.map((item: UploadingFileItem) => item.note_id) 42 | ) 43 | 44 | this.$state.notes.forEach((note: NoteItem) => { 45 | note.isUploading = noteIdsSet.has(note.id) 46 | }) 47 | }, 48 | }, 49 | }) 50 | -------------------------------------------------------------------------------- /app/src/utils/color-switch.ts: -------------------------------------------------------------------------------- 1 | import { useDark } from '@vueuse/core' 2 | import { computed } from 'vue' 3 | 4 | export const defaultBgColor = computed(() => { 5 | const isDark = useDark() 6 | return isDark.value ? 'grey-darken-4' : 'grey-lighten-5' 7 | }) 8 | 9 | export const greenBgColor = computed(() => { 10 | const isDark = useDark() 11 | return isDark.value ? 'green-accent-3' : 'light-green-accent-3' 12 | }) 13 | 14 | export const orangeBgColor = computed(() => { 15 | const isDark = useDark() 16 | return isDark.value ? 'orange-darken-3' : 'orange-accent-2' 17 | }) 18 | 19 | export const fontColor = computed(() => { 20 | const isDark = useDark() 21 | return isDark.value 22 | ? { 23 | color: 'white', 24 | } 25 | : { 26 | color: 'black', 27 | } 28 | }) 29 | 30 | export const greenBorderColor = computed(() => { 31 | const isDark = useDark() 32 | return isDark.value ? 'green-accent-2' : 'green-accent-4' 33 | }) 34 | 35 | export const theme = computed(() => { 36 | const isDark = useDark() 37 | return isDark.value ? 'light' : 'dark' 38 | }) 39 | 40 | export const reverseTheme = computed(() => { 41 | const isDark = useDark() 42 | return isDark.value ? 'dark' : 'light' 43 | }) 44 | -------------------------------------------------------------------------------- /app/src/utils/date-handler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts the date portion from a datetime string in 'YYYY-MM-DDTHH:mm:ss' format. 3 | * 4 | * @param {string} datetime - The input datetime string. 5 | * @returns {string} The date portion extracted from the datetime string, or the original string if the format is invalid. 6 | * @example "xxxx-xx-xxT00:00:00" to "xxxx-xx-xx" 7 | */ 8 | export const handleDatetime = (datatime: string): string => { 9 | const chunks = datatime.split('T') 10 | if (chunks.length <= 1) return datatime 11 | return chunks[0] 12 | } 13 | -------------------------------------------------------------------------------- /app/src/utils/detect-legal-file.ts: -------------------------------------------------------------------------------- 1 | import { MessagePlugin, type UploadFile } from 'tdesign-vue-next' 2 | 3 | const MIME = { 4 | MARKDOWN: 'text/markdown', 5 | X_MARKDOWN: 'text/x-markdown', 6 | TEXT_PLAIN: 'text/plain', 7 | OCTET_STREAM: 'application/octet-stream', 8 | } 9 | 10 | /** 11 | * Checks if a given file has a valid type for uploading. 12 | * 13 | * @param file - The file object to be checked. 14 | * @returns Returns true if the file type is valid, otherwise returns false. 15 | */ 16 | export const detectLegalFile = (file: UploadFile): boolean => { 17 | if (!isValidMarkdownFile(file)) { 18 | MessagePlugin.warning( 19 | `The type of file '${file.name}' is wrong, Only "Markdown" type files are allowed to be uploaded` 20 | ) 21 | return false 22 | } else return true 23 | } 24 | 25 | /** 26 | * Checks if a given file is a valid Markdown file based on its name and MIME type. 27 | * 28 | * @param file - The file to check. 29 | * @returns Returns true if the file is a valid Markdown file, otherwise false. 30 | */ 31 | const isValidMarkdownFile = (file: UploadFile): boolean => { 32 | const { name, type } = file 33 | if (!name!.endsWith('.md')) return false 34 | return true 35 | 36 | // dont delete --- 37 | // if ( 38 | // type === MIME.MARKDOWN || 39 | // type === MIME.X_MARKDOWN || 40 | // type === MIME.TEXT_PLAIN || 41 | // type === MIME.OCTET_STREAM 42 | // ) { 43 | // return true 44 | // } else return false 45 | } 46 | -------------------------------------------------------------------------------- /app/src/utils/download.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Download the binary delivered by the backend 3 | * 4 | * @param data the binary file data 5 | * @param filename the file name 6 | * @param MIME the MIME type of the file 7 | */ 8 | export const dowmloadBinaryFile = ( 9 | data: any, 10 | filename: string, 11 | MIME: string 12 | ): void => { 13 | const url = window.URL.createObjectURL(new Blob([data], { type: MIME })) 14 | const link = document.createElement('a') 15 | link.style.display = 'none' 16 | link.href = url 17 | link.setAttribute('download', filename) 18 | document.body.appendChild(link) 19 | link.click() 20 | document.body.removeChild(link) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color-switch' 2 | export * from './detect-legal-file' 3 | export * from './download' 4 | export * from './date-handler' 5 | export * from './storage-hanlder' 6 | export * from './to-markdown' 7 | export * from './scroll-handler' 8 | -------------------------------------------------------------------------------- /app/src/utils/scroll-handler.ts: -------------------------------------------------------------------------------- 1 | import { useScroll } from '@vueuse/core' 2 | 3 | export const scrollToPageBottom = () => { 4 | const { y } = useScroll(window, { behavior: 'smooth' }) 5 | 6 | y.value = document.documentElement.scrollHeight 7 | } 8 | -------------------------------------------------------------------------------- /app/src/utils/storage-hanlder.ts: -------------------------------------------------------------------------------- 1 | import { useNow, useDateFormat } from '@vueuse/core' 2 | 3 | const today = useDateFormat(useNow(), 'YYYY-MM-DD') 4 | 5 | /** 6 | * Clears expired storage data from the local storage. 7 | * 8 | */ 9 | export const clearExipredStorageData = () => { 10 | let i = 0 11 | while (localStorage.key(i)) { 12 | const key: string = localStorage.key(i)! 13 | let chunks: string[] = [] 14 | if (key.includes(':')) chunks = key.split(':') 15 | if (checkIsExpired(chunks[0])) localStorage.removeItem(key) 16 | i++ 17 | } 18 | } 19 | 20 | /** 21 | * Checks if a given day is expired. 22 | * 23 | * @param {string} day - The day to check for expiration. 24 | * @returns {boolean} Returns true if the day is expired, otherwise false. 25 | */ 26 | const checkIsExpired = (day: string) => { 27 | if (!day) return false 28 | const date1 = new Date(day).getTime() 29 | const date2 = new Date(today.value).getTime() 30 | if (date1 < date2) return true 31 | else false 32 | } 33 | 34 | /** 35 | * Checks if the answer for a specific question is cached in local storage. 36 | * 37 | * @param {string} questionId - The ID of the question to check. 38 | * @returns {boolean} Returns true if the answer is cached, otherwise false. 39 | */ 40 | export const checkAnswerIsInCache = (questionId: string) => { 41 | let i = 0 42 | while (localStorage.key(i)) { 43 | const key: string = localStorage.key(i)! 44 | let chunks: string[] = [] 45 | if (key.includes(':')) chunks = key.split(':') 46 | if (chunks[1] === questionId) { 47 | return true 48 | } 49 | i++ 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /app/src/utils/to-markdown.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it' 2 | import hljs from 'highlight.js' 3 | 4 | /** 5 | * Converts the given text to Markdown format using MarkdownIt library. 6 | * 7 | * @param {string} text - The input text to be converted to Markdown. 8 | * @returns {string} The Markdown formatted text. 9 | */ 10 | export const toMarkdown = (text: string) => { 11 | const md = new MarkdownIt({ 12 | highlight: function (str, lang) { 13 | if (lang && hljs.getLanguage(lang)) 14 | return hljs.highlight(str, { language: lang }).value 15 | return '' 16 | }, 17 | html: true, 18 | linkify: true, 19 | typographer: true, 20 | breaks: true, 21 | }) 22 | return md.render(text) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/views/AddNote.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | 31 | 37 | 38 | 48 | -------------------------------------------------------------------------------- /app/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/views/Random.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 36 | 64 | -------------------------------------------------------------------------------- /app/src/views/Welcome.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": [ 14 | "ESNext", 15 | "DOM" 16 | ], 17 | "skipLibCheck": true, 18 | "noEmit": true, 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | } 24 | }, 25 | "include": [ 26 | "src/**/*.ts", 27 | "src/**/*.d.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue" 30 | ], 31 | "references": [ 32 | { 33 | "path": "./tsconfig.node.json" 34 | } 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } -------------------------------------------------------------------------------- /app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 3 | import AutoImport from 'unplugin-auto-import/vite' 4 | import Components from 'unplugin-vue-components/vite' 5 | import { TDesignResolver } from 'unplugin-vue-components/resolvers' 6 | import { defineConfig } from 'vite' 7 | import { fileURLToPath, URL } from 'node:url' 8 | 9 | export default defineConfig({ 10 | plugins: [ 11 | vue({ 12 | template: { transformAssetUrls }, 13 | }), 14 | vuetify({ 15 | autoImport: true, 16 | }), 17 | AutoImport({ 18 | resolvers: [ 19 | TDesignResolver({ 20 | library: 'vue-next', 21 | }), 22 | ], 23 | }), 24 | Components({ 25 | resolvers: [ 26 | TDesignResolver({ 27 | library: 'vue-next', 28 | }), 29 | ], 30 | }), 31 | ], 32 | 33 | define: { 'process.env': {} }, 34 | 35 | resolve: { 36 | alias: { 37 | '@': fileURLToPath(new URL('./src', import.meta.url)), 38 | }, 39 | extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'], 40 | }, 41 | 42 | server: { 43 | port: 51818, 44 | proxy: { 45 | '/api': { 46 | target: `http://${process.env.DOCKER ? 'server' : '0.0.0.0'}:51717`, 47 | changeOrigin: true, 48 | rewrite: (pathStr) => pathStr.replace('/api', '/'), 49 | }, 50 | '/ws': { 51 | target: `ws://${process.env.DOCKER ? 'server' : 'localhost'}:51717`, 52 | changeOrigin: true, 53 | ws: true, 54 | rewrite: (pathStr) => pathStr.replace('/ws', '/'), 55 | }, 56 | }, 57 | }, 58 | }) 59 | -------------------------------------------------------------------------------- /database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0 2 | 3 | ENV MYSQL_ROOT_PASSWORD='root' 4 | ENV MYSQL_DATABASE='db' 5 | 6 | COPY ./database.sql /docker-entrypoint-initdb.d/ 7 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: ./app/ 5 | command: ["sh", "-c", "sleep 15 && pnpm dev:docker"] 6 | restart: always 7 | depends_on: 8 | - server 9 | ports: 10 | - '51818:51818' 11 | 12 | server: 13 | build: ./server/ 14 | restart: always 15 | depends_on: 16 | - database 17 | ports: 18 | - '51717:51717' 19 | 20 | database: 21 | build: ./database/ 22 | restart: always 23 | ports: 24 | - '52020:3306' 25 | 26 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | > [!IMPORTANT] 4 | > v0.5.0 - 85% Progress 5 | 6 | ### Features 7 | 8 | - [x] ✨Add `question-bank` module 9 | 10 | The questions generated by GPT-4 and their source documents will be stored in the question bank module. In this module, users can choose which question banks to import as their own note questions. 11 | - [ ] Dashboard page 12 | - [ ] Support PWA 13 | - [ ] Support more document types 14 | - [ ] `.docs` 15 | - [ ] `.pdf` 16 | - [ ] notionDB 17 | - [x] Generate more question types 18 | - [x] single choice 19 | - [x] fill in the blanks 20 | - [ ] Add more model options 21 | - [x] gpt-4 (openai) 22 | - [x] claude-2 (anthropic) 23 | - [ ] llama 24 | - [x] Allow users to select question types when uploading docs 25 | - [x] Add detection modules for different question types 26 | - [x] Added data import and export features to back up data 27 | - [x] Provide identity selection, allowing users to set the identity of teachers, examiners, interviewers, etc. that affect the strictness of detection and the divergence of generated questions 28 | 29 | | Role | Divergence | Strictness | 30 | | ----------- | ---------- | ---------- | 31 | | Teacher | ⭐️⭐️ | 😏 | 32 | | Interviewer | ⭐️⭐️⭐️ | 😐 | 33 | | Examiner | ⭐️ | 😭 | 34 | - [ ] Custom prompt: A module that allows users to set their own question-generation prompts, customize roles, and question types. 35 | 36 | ### Optimizations 37 | 38 | - [ ] Optimize prompt templates for question generation 39 | - [ ] Optimize prompt templates for examine 40 | - [ ] Optimize Ebbinghaus algorithm 41 | - [x] Improve error messages 42 | -------------------------------------------------------------------------------- /docs/best-practices/processing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/best-practices/processing.png -------------------------------------------------------------------------------- /docs/best-practices/zh-best-practices.md: -------------------------------------------------------------------------------- 1 | # 文档最佳实践 2 | 3 | ## 上传文档的过程 4 | 5 | 首先先了解一下整个上传文档的过程会发生什么 6 | 7 | 8 | 9 | 1. 我首先会通过 langchain 的 `RecursiveCharacterTextSplitter` 类对文档进行切块, 切分成若干个 Chunk 10 | 11 | 2. 在获得 Chunk 后, 我会对 Chunk 及逆行一系列的检测, 涉及的函数有 `is_odd_backtick_paired`, `is_there_no_enough_content`, `is_the_token_exceeded` 这些函数都可以在 [share.py](https://github.com/codeacme17/examor/blob/main/server/loaders/share.py) 这个文件中看到 12 | 13 | 3. 最后将切分好的 Chunk 拼接到 Prompt 中发送给 LLM 为我们生成问题, 将问题存入到数据库中 14 | 15 | ## 文档格式 16 | 17 | 从上述的过程中可以看到, 我们会有一个切片的过程, 这也就意味着 **更好文档格式会切出更好的效果, 这同时会影响 LLM 问题生成的质量**. 虽然我对切片的逻辑进行了很大程度上的优化, 让他可以尽可能的去匹配多样的格式, 但这并不是万能的, 所以还是建议用户遵循以下几点的文档格式 18 | 19 | ### 明确的标题 20 | 21 | 这里的标题并不是单指 `#1` 这样的大标题, 而是指每一个段落都应该有其严格的标题进行引领. 22 | 23 | ```markdown 24 | ✔️ 25 | 26 | # Hello World 27 | 28 | some content 29 | 30 | ## How to see hello 31 | 32 | some content 33 | ``` 34 | 35 | ```markdown 36 | ❌ 37 | 38 | # Hello World 39 | 40 | some content 41 | How to see hello 42 | some content 43 | ``` 44 | 45 | ### 不建议过长的代码块 46 | 47 | 不建议在文档中存在内容过长且无注释的代码块(超出 2500 token), 因为这样的 Chunk 中可能只携带了代码而无内容描述, 这会导致 LLM 无法生成优质的问题. 如果您认为您文档中的代码很重要, 那么请您给予他们注释, 并对其进行有条理的拆分 48 | 49 | ````markdown 50 | ✔️ 51 | 52 | ```js 53 | // this is a sum funciton to sum a and b 54 | fucntion sum(a, b) { 55 | // .... 56 | // lines 2000 57 | // ... 58 | } 59 | ``` 60 | ```` 61 | 62 | ````markdown 63 | ❌ 64 | 65 | ```js 66 | fucntion sum(a, b) { 67 | // .... 68 | // lines 2000 69 | // ... 70 | } 71 | ``` 72 | ```` 73 | 74 | ### 案例文档 75 | 76 | 我准备了 vue 仓库中的有关描述 `props` 的[文档](https://github.com/codeacme17/examor/blob/main/docs/templates/zh-vue-props.md) , 他有着十分友好的文档格式, 推荐使用该文档进行借鉴或者测试 77 | -------------------------------------------------------------------------------- /docs/contribute/en-bank.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Question Bank 2 | 3 | > [!IMPORTANT] 4 | > The Question Bank is a module newly added in version `v0.4.0`, so we haven't collected many resources yet. We hope you can join us in enriching our bank! 5 | 6 | ## How to Contribute 7 | 8 | If you have recommended resources for the question bank, you can contribute in the following two ways: 9 | 10 | ### PR 11 | 12 | Submitting a PR is the recommended method. When doing so, please follow these steps: 13 | 14 | 1. Fork your own branch. 15 | 16 | 2. Navigate to the [question-bank](https://github.com/codeacme17/examor/tree/main/docs/question-bank) folder. 17 | 18 | 3. Determine the language of the document; for English documents, enter the `en` folder. 19 | 20 | 4. Identify the category of your resource. If the current repository doesn't have a category that fits your resource, you can create a new folder and name it according to the category (e.g., resources related to programming are placed in the `programming` folder). 21 | 22 | 5. Create a folder for your resource, with the **folder name being the name of the resource** (this is crucial). 23 | 24 | 6. Filter by file type; currently, only `.md` type files are supported. 25 | 26 | 7. When submitting a PR, please provide the URL, description, and Icon of the resource in the PR. While these details are not mandatory, they are highly recommended. 27 | 28 | If your resource is incorporated into our question bank, we'll mark you as a contributor to that resource in its card, linking to your GitHub, as a token of our gratitude! 29 | 30 | ### Issue or Discussion 31 | 32 | You can also notify me of any recommended high-quality resources for inclusion in the [Issue](https://github.com/codeacme17/examor/issues) or [Discussion](https://github.com/codeacme17/examor/discussions) sections. 33 | 34 | ## Currently Included Resources (English) 35 | 36 | ### Programming Category 37 | 38 | - [vue-component](https://vuejs.org/guide/components/registration.html) 39 | - [vue-apis](https://vuejs.org/api/) 40 | -------------------------------------------------------------------------------- /docs/contribute/zh-bank.md: -------------------------------------------------------------------------------- 1 | # 贡献题库 2 | 3 | > [!IMPORTANT] 4 | > 题库是 `v0.4.0` 版本中新增的模块,所以并没有收集到太多的资源,很希望您可以与我一起丰富我们的题库! 5 | 6 | ## 如何贡献 7 | 8 | 如果您有推荐加入的题库,可以以下两种方式进行贡献 9 | 10 | ### PR 11 | 12 | 提交一个 PR 是比较推荐的方式,在提交 PR 的时候需要先做一下几步 13 | 14 | 1. fork 一个自己的分支 15 | 16 | 2. 进入到 [question-bank](https://github.com/codeacme17/examor/tree/main/docs/question-bank) 文件夹中 17 | 18 | 3. 请确认文档的语言,中文文档进入到 zh 文件夹下 19 | 20 | 4. 请确认资源的分类,如果当前仓库中没有符合您资源的分类,可以创建一个文件夹并以分类名进行命名 (如 `programming` 中放置的是编程相关的资源) 21 | 22 | 5. 请为您的资源创建一个文件夹,**文件名是这个资源的名称**(这很重要) 23 | 24 | 6. 请筛选资源的文件类型,目前仅支持 `.md` 类型的文件 25 | 26 | 7. 在提交 PR 的时候请在 PR 中写入该资源的 URL、描述和 Icon,当然这三个都不是必须的只是比较推荐写入这些信息 27 | 28 | 如果您的资源被收录到了我们的题库中,在该题库的 card 中,我将您作为该题库的贡献者标识您的 github,以表我对您的感谢! 29 | 30 | ### Issue or Discussion 31 | 32 | 您也可以在 [Issue](https://github.com/codeacme17/examor/issues) 或 [Discussion](https://github.com/codeacme17/examor/discussions) 中告知我有哪些优质的资源推荐收录 33 | 34 | ## 目前收录的题库(中文) 35 | 36 | ### 编程类 37 | 38 | - [vue-component](https://cn.vuejs.org/guide/components/registration.html) 39 | - [vue-apis](https://cn.vuejs.org/api/) 40 | - [hello-algo](https://www.hello-algo.com/) 41 | -------------------------------------------------------------------------------- /docs/logo-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/logo-banner.png -------------------------------------------------------------------------------- /docs/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/logo-text.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/logo.png -------------------------------------------------------------------------------- /docs/product-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/product-full.png -------------------------------------------------------------------------------- /docs/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/product.png -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/binary_search_edge.md: -------------------------------------------------------------------------------- 1 | # 二分查找边界 2 | 3 | ## 查找左边界 4 | 5 | !!! question 6 | 7 | 给定一个长度为 $n$ 的有序数组 `nums` ,数组可能包含重复元素。请返回数组中最左一个元素 `target` 的索引。若数组中不包含该元素,则返回 $-1$ 。 8 | 9 | 回忆二分查找插入点的方法,搜索完成后 $i$ 指向最左一个 `target` ,**因此查找插入点本质上是在查找最左一个 `target` 的索引**。 10 | 11 | 考虑通过查找插入点的函数实现查找左边界。请注意,数组中可能不包含 `target` ,这种情况可能导致以下两种结果。 12 | 13 | - 插入点的索引 $i$ 越界。 14 | - 元素 `nums[i]` 与 `target` 不相等。 15 | 16 | 当遇到以上两种情况时,直接返回 $-1$ 即可。 17 | 18 | ```src 19 | [file]{binary_search_edge}-[class]{}-[func]{binary_search_left_edge} 20 | ``` 21 | 22 | ## 查找右边界 23 | 24 | 那么如何查找最右一个 `target` 呢?最直接的方式是修改代码,替换在 `nums[m] == target` 情况下的指针收缩操作。代码在此省略,有兴趣的同学可以自行实现。 25 | 26 | 下面我们介绍两种更加取巧的方法。 27 | 28 | ### 复用查找左边界 29 | 30 | 实际上,我们可以利用查找最左元素的函数来查找最右元素,具体方法为:**将查找最右一个 `target` 转化为查找最左一个 `target + 1`**。 31 | 32 | 如下图所示,查找完成后,指针 $i$ 指向最左一个 `target + 1`(如果存在),而 $j$ 指向最右一个 `target` ,**因此返回 $j$ 即可**。 33 | 34 | ![将查找右边界转化为查找左边界](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png) 35 | 36 | 请注意,返回的插入点是 $i$ ,因此需要将其减 $1$ ,从而获得 $j$ 。 37 | 38 | ```src 39 | [file]{binary_search_edge}-[class]{}-[func]{binary_search_right_edge} 40 | ``` 41 | 42 | ### 转化为查找元素 43 | 44 | 我们知道,当数组不包含 `target` 时,最终 $i$ 和 $j$ 会分别指向首个大于、小于 `target` 的元素。 45 | 46 | 因此,如下图所示,我们可以构造一个数组中不存在的元素,用于查找左右边界。 47 | 48 | - 查找最左一个 `target` :可以转化为查找 `target - 0.5` ,并返回指针 $i$ 。 49 | - 查找最右一个 `target` :可以转化为查找 `target + 0.5` ,并返回指针 $j$ 。 50 | 51 | ![将查找边界转化为查找元素](binary_search_edge.assets/binary_search_edge_by_element.png) 52 | 53 | 代码在此省略,值得注意以下两点。 54 | 55 | - 给定数组不包含小数,这意味着我们无须关心如何处理相等的情况。 56 | - 因为该方法引入了小数,所以需要将函数中的变量 `target` 改为浮点数类型。 57 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/binary_search_recur.md: -------------------------------------------------------------------------------- 1 | # 分治搜索策略 2 | 3 | 我们已经学过,搜索算法分为两大类。 4 | 5 | - **暴力搜索**:它通过遍历数据结构实现,时间复杂度为 $O(n)$ 。 6 | - **自适应搜索**:它利用特有的数据组织形式或先验信息,可达到 $O(\log n)$ 甚至 $O(1)$ 的时间复杂度。 7 | 8 | 实际上,**时间复杂度为 $O(\log n)$ 的搜索算法通常都是基于分治策略实现的**,例如二分查找和树。 9 | 10 | - 二分查找的每一步都将问题(在数组中搜索目标元素)分解为一个小问题(在数组的一半中搜索目标元素),这个过程一直持续到数组为空或找到目标元素为止。 11 | - 树是分治关系的代表,在二叉搜索树、AVL 树、堆等数据结构中,各种操作的时间复杂度皆为 $O(\log n)$ 。 12 | 13 | 二分查找的分治策略如下所示。 14 | 15 | - **问题可以被分解**:二分查找递归地将原问题(在数组中进行查找)分解为子问题(在数组的一半中进行查找),这是通过比较中间元素和目标元素来实现的。 16 | - **子问题是独立的**:在二分查找中,每轮只处理一个子问题,它不受另外子问题的影响。 17 | - **子问题的解无须合并**:二分查找旨在查找一个特定元素,因此不需要将子问题的解进行合并。当子问题得到解决时,原问题也会同时得到解决。 18 | 19 | 分治能够提升搜索效率,本质上是因为暴力搜索每轮只能排除一个选项,**而分治搜索每轮可以排除一半选项**。 20 | 21 | ### 基于分治实现二分 22 | 23 | 在之前的章节中,二分查找是基于递推(迭代)实现的。现在我们基于分治(递归)来实现它。 24 | 25 | !!! question 26 | 27 | 给定一个长度为 $n$ 的有序数组 `nums` ,数组中所有元素都是唯一的,请查找元素 `target` 。 28 | 29 | 从分治角度,我们将搜索区间 $[i, j]$ 对应的子问题记为 $f(i, j)$ 。 30 | 31 | 从原问题 $f(0, n-1)$ 为起始点,通过以下步骤进行二分查找。 32 | 33 | 1. 计算搜索区间 $[i, j]$ 的中点 $m$ ,根据它排除一半搜索区间。 34 | 2. 递归求解规模减小一半的子问题,可能为 $f(i, m-1)$ 或 $f(m+1, j)$ 。 35 | 3. 循环第 `1.` 和 `2.` 步,直至找到 `target` 或区间为空时返回。 36 | 37 | 下图展示了在数组中二分查找元素 $6$ 的分治过程。 38 | 39 | ![二分查找的分治过程](binary_search_recur.assets/binary_search_recur.png) 40 | 41 | 在实现代码中,我们声明一个递归函数 `dfs()` 来求解问题 $f(i, j)$ 。 42 | 43 | ```src 44 | [file]{binary_search_recur}-[class]{}-[func]{binary_search} 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/bubble_sort.md: -------------------------------------------------------------------------------- 1 | # 冒泡排序 2 | 3 | 「冒泡排序 bubble sort」通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。 4 | 5 | 如下图所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端。 6 | 7 | === "<1>" 8 | ![利用元素交换操作模拟冒泡](bubble_sort.assets/bubble_operation_step1.png) 9 | 10 | === "<2>" 11 | ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png) 12 | 13 | === "<3>" 14 | ![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png) 15 | 16 | === "<4>" 17 | ![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png) 18 | 19 | === "<5>" 20 | ![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png) 21 | 22 | === "<6>" 23 | ![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png) 24 | 25 | === "<7>" 26 | ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png) 27 | 28 | ## 算法流程 29 | 30 | 设数组的长度为 $n$ ,冒泡排序的步骤如下图所示。 31 | 32 | 1. 首先,对 $n$ 个元素执行“冒泡”,**将数组的最大元素交换至正确位置**, 33 | 2. 接下来,对剩余 $n - 1$ 个元素执行“冒泡”,**将第二大元素交换至正确位置**。 34 | 3. 以此类推,经过 $n - 1$ 轮“冒泡”后,**前 $n - 1$ 大的元素都被交换至正确位置**。 35 | 4. 仅剩的一个元素必定是最小元素,无须排序,因此数组排序完成。 36 | 37 | ![冒泡排序流程](bubble_sort.assets/bubble_sort_overview.png) 38 | 39 | ```src 40 | [file]{bubble_sort}-[class]{}-[func]{bubble_sort} 41 | ``` 42 | 43 | ## 效率优化 44 | 45 | 我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 `flag` 来监测这种情况,一旦出现就立即返回。 46 | 47 | 经过优化,冒泡排序的最差和平均时间复杂度仍为 $O(n^2)$ ;但当输入数组完全有序时,可达到最佳时间复杂度 $O(n)$ 。 48 | 49 | ```src 50 | [file]{bubble_sort}-[class]{}-[func]{bubble_sort_with_flag} 51 | ``` 52 | 53 | ## 算法特性 54 | 55 | - **时间复杂度为 $O(n^2)$、自适应排序**:各轮“冒泡”遍历的数组长度依次为 $n - 1$、$n - 2$、$\dots$、$2$、$1$ ,总和为 $(n - 1) n / 2$ 。在引入 `flag` 优化后,最佳时间复杂度可达到 $O(n)$ 。 56 | - **空间复杂度为 $O(1)$、原地排序**:指针 $i$ 和 $j$ 使用常数大小的额外空间。 57 | - **稳定排序**:由于在“冒泡”中遇到相等元素不交换。 58 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/bucket_sort.md: -------------------------------------------------------------------------------- 1 | # 桶排序 2 | 3 | 前述的几种排序算法都属于“基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 $O(n \log n)$ 。接下来,我们将探讨几种“非比较排序算法”,它们的时间复杂度可以达到线性阶。 4 | 5 | 「桶排序 bucket sort」是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。 6 | 7 | ## 算法流程 8 | 9 | 考虑一个长度为 $n$ 的数组,元素是范围 $[0, 1)$ 的浮点数。桶排序的流程如下图所示。 10 | 11 | 1. 初始化 $k$ 个桶,将 $n$ 个元素分配到 $k$ 个桶中。 12 | 2. 对每个桶分别执行排序(本文采用编程语言的内置排序函数)。 13 | 3. 按照桶的从小到大的顺序,合并结果。 14 | 15 | ![桶排序算法流程](bucket_sort.assets/bucket_sort_overview.png) 16 | 17 | ```src 18 | [file]{bucket_sort}-[class]{}-[func]{bucket_sort} 19 | ``` 20 | 21 | ## 算法特性 22 | 23 | 桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。 24 | 25 | - **时间复杂度 $O(n + k)$** :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 $\frac{n}{k}$ 。假设排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,则排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 比较大时,时间复杂度则趋向于 $O(n)$** 。合并结果时需要遍历所有桶和元素,花费 $O(n + k)$ 时间。 26 | - **自适应排序**:在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 $O(n^2)$ 时间。 27 | - **空间复杂度 $O(n + k)$、非原地排序**:需要借助 $k$ 个桶和总共 $n$ 个元素的额外空间。 28 | - 桶排序是否稳定取决于排序桶内元素的算法是否稳定。 29 | 30 | ## 如何实现平均分配 31 | 32 | 桶排序的时间复杂度理论上可以达到 $O(n)$ ,**关键在于将元素均匀分配到各个桶中**,因为实际数据往往不是均匀分布的。例如,我们想要将淘宝上的所有商品按价格范围平均分配到 10 个桶中,但商品价格分布不均,低于 100 元的非常多,高于 1000 元的非常少。若将价格区间平均划分为 10 份,各个桶中的商品数量差距会非常大。 33 | 34 | 为实现平均分配,我们可以先设定一个大致的分界线,将数据粗略地分到 3 个桶中。**分配完毕后,再将商品较多的桶继续划分为 3 个桶,直至所有桶中的元素数量大致相等**。 35 | 36 | 如下图所示,这种方法本质上是创建一个递归树,目标是让叶节点的值尽可能平均。当然,不一定要每轮将数据划分为 3 个桶,具体划分方式可根据数据特点灵活选择。 37 | 38 | ![递归划分桶](bucket_sort.assets/scatter_in_buckets_recursively.png) 39 | 40 | 如果我们提前知道商品价格的概率分布,**则可以根据数据概率分布设置每个桶的价格分界线**。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。 41 | 42 | 如下图所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。 43 | 44 | ![根据概率分布划分桶](bucket_sort.assets/scatter_in_buckets_distribution.png) 45 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/classification_of_data_structure.md: -------------------------------------------------------------------------------- 1 | # 数据结构分类 2 | 3 | 常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。 4 | 5 | ## 逻辑结构:线性与非线性 6 | 7 | **逻辑结构揭示了数据元素之间的逻辑关系**。在数组和链表中,数据按照顺序依次排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出祖先与后代之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。 8 | 9 | 如下图所示,逻辑结构可被分为“线性”和“非线性”两大类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。 10 | 11 | - **线性数据结构**:数组、链表、栈、队列、哈希表。 12 | - **非线性数据结构**:树、堆、图、哈希表。 13 | 14 | ![线性与非线性数据结构](classification_of_data_structure.assets/classification_logic_structure.png) 15 | 16 | 非线性数据结构可以进一步被划分为树形结构和网状结构。 17 | 18 | - **线性结构**:数组、链表、队列、栈、哈希表,元素之间是一对一的顺序关系。 19 | - **树形结构**:树、堆、哈希表,元素之间是一对多的关系。 20 | - **网状结构**:图,元素之间是多对多的关系。 21 | 22 | ## 物理结构:连续与分散 23 | 24 | 在计算机中,内存和硬盘是两种主要的存储硬件设备。硬盘主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。内存用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。 25 | 26 | **在算法运行过程中,相关数据都存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据,在算法运行时,所有数据都被存储在这些单元格中。 27 | 28 | **系统通过内存地址来访问目标位置的数据**。如下图所示,计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。 29 | 30 | ![内存条、内存空间、内存地址](classification_of_data_structure.assets/computer_memory_location.png) 31 | 32 | 内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用了。**因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素**。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。 33 | 34 | 如下图所示,**物理结构反映了数据在计算机内存中的存储方式**,可分为连续空间存储(数组)和分散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,同时在时间效率和空间效率方面呈现出互补的特点。 35 | 36 | ![连续空间存储与分散空间存储](classification_of_data_structure.assets/classification_phisical_structure.png) 37 | 38 | 值得说明的是,**所有数据结构都是基于数组、链表或二者的组合实现的**。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。 39 | 40 | - **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等。 41 | - **基于链表可实现**:栈、队列、哈希表、树、堆、图等。 42 | 43 | 基于数组实现的数据结构也被称为“静态数据结构”,这意味着此类数据结构在初始化后长度不可变。相对应地,基于链表实现的数据结构被称为“动态数据结构”,这类数据结构在初始化后,仍可以在程序运行过程中对其长度进行调整。 44 | 45 | !!! tip 46 | 47 | 如果你感觉物理结构理解起来有困难,建议先阅读下一章“数组与链表”,然后再回顾本节内容。 48 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/fractional_knapsack_problem.md: -------------------------------------------------------------------------------- 1 | # 分数背包问题 2 | 3 | !!! question 4 | 5 | 给定 $n$ 个物品,第 $i$ 个物品的重量为 $wgt[i-1]$、价值为 $val[i-1]$ ,和一个容量为 $cap$ 的背包。每个物品只能选择一次,**但可以选择物品的一部分,价值根据选择的重量比例计算**,问在不超过背包容量下背包中物品的最大价值。 6 | 7 | ![分数背包问题的示例数据](fractional_knapsack_problem.assets/fractional_knapsack_example.png) 8 | 9 | 分数背包和 0-1 背包整体上非常相似,状态包含当前物品 $i$ 和容量 $c$ ,目标是求不超过背包容量下的最大价值。 10 | 11 | 不同点在于,本题允许只选择物品的一部分。如下图所示,**我们可以对物品任意地进行切分,并按照重量比例来计算物品价值**。 12 | 13 | 1. 对于物品 $i$ ,它在单位重量下的价值为 $val[i-1] / wgt[i-1]$ ,简称为单位价值。 14 | 2. 假设放入一部分物品 $i$ ,重量为 $w$ ,则背包增加的价值为 $w \times val[i-1] / wgt[i-1]$ 。 15 | 16 | ![物品在单位重量下的价值](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png) 17 | 18 | ### 贪心策略确定 19 | 20 | 最大化背包内物品总价值,**本质上是要最大化单位重量下的物品价值**。由此便可推出下图所示的贪心策略。 21 | 22 | 1. 将物品按照单位价值从高到低进行排序。 23 | 2. 遍历所有物品,**每轮贪心地选择单位价值最高的物品**。 24 | 3. 若剩余背包容量不足,则使用当前物品的一部分填满背包即可。 25 | 26 | ![分数背包的贪心策略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png) 27 | 28 | ### 代码实现 29 | 30 | 我们建立了一个物品类 `Item` ,以便将物品按照单位价值进行排序。循环进行贪心选择,当背包已满时跳出并返回解。 31 | 32 | ```src 33 | [file]{fractional_knapsack}-[class]{}-[func]{fractional_knapsack} 34 | ``` 35 | 36 | 最差情况下,需要遍历整个物品列表,**因此时间复杂度为 $O(n)$** ,其中 $n$ 为物品数量。 37 | 38 | 由于初始化了一个 `Item` 对象列表,**因此空间复杂度为 $O(n)$** 。 39 | 40 | ### 正确性证明 41 | 42 | 采用反证法。假设物品 $x$ 是单位价值最高的物品,使用某算法求得最大价值为 `res` ,但该解中不包含物品 $x$ 。 43 | 44 | 现在从背包中拿出单位重量的任意物品,并替换为单位重量的物品 $x$ 。由于物品 $x$ 的单位价值最高,因此替换后的总价值一定大于 `res` 。**这与 `res` 是最优解矛盾,说明最优解中必须包含物品 $x$** 。 45 | 46 | 对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,**单位价值更大的物品总是更优选择**,这说明贪心策略是有效的。 47 | 48 | 如下图所示,如果将物品重量和物品单位价值分别看作一个 2D 图表的横轴和纵轴,则分数背包问题可被转化为“求在有限横轴区间下的最大围成面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。 49 | 50 | ![分数背包问题的几何表示](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png) 51 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/heap_sort.md: -------------------------------------------------------------------------------- 1 | # 堆排序 2 | 3 | !!! tip 4 | 5 | 阅读本节前,请确保已学完“堆“章节。 6 | 7 | 「堆排序 heap sort」是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序。 8 | 9 | 1. 输入数组并建立小顶堆,此时最小元素位于堆顶。 10 | 2. 不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。 11 | 12 | 以上方法虽然可行,但需要借助一个额外数组来保存弹出的元素,比较浪费空间。在实际中,我们通常使用一种更加优雅的实现方式。 13 | 14 | ## 算法流程 15 | 16 | 设数组的长度为 $n$ ,堆排序的流程如下图所示。 17 | 18 | 1. 输入数组并建立大顶堆。完成后,最大元素位于堆顶。 19 | 2. 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 $1$ ,已排序元素数量加 $1$ 。 20 | 3. 从堆顶元素开始,从顶到底执行堆化操作(Sift Down)。完成堆化后,堆的性质得到修复。 21 | 4. 循环执行第 `2.` 和 `3.` 步。循环 $n - 1$ 轮后,即可完成数组排序。 22 | 23 | !!! tip 24 | 25 | 实际上,元素出堆操作中也包含第 `2.` 和 `3.` 步,只是多了一个弹出元素的步骤。 26 | 27 | === "<1>" 28 | ![堆排序步骤](heap_sort.assets/heap_sort_step1.png) 29 | 30 | === "<2>" 31 | ![heap_sort_step2](heap_sort.assets/heap_sort_step2.png) 32 | 33 | === "<3>" 34 | ![heap_sort_step3](heap_sort.assets/heap_sort_step3.png) 35 | 36 | === "<4>" 37 | ![heap_sort_step4](heap_sort.assets/heap_sort_step4.png) 38 | 39 | === "<5>" 40 | ![heap_sort_step5](heap_sort.assets/heap_sort_step5.png) 41 | 42 | === "<6>" 43 | ![heap_sort_step6](heap_sort.assets/heap_sort_step6.png) 44 | 45 | === "<7>" 46 | ![heap_sort_step7](heap_sort.assets/heap_sort_step7.png) 47 | 48 | === "<8>" 49 | ![heap_sort_step8](heap_sort.assets/heap_sort_step8.png) 50 | 51 | === "<9>" 52 | ![heap_sort_step9](heap_sort.assets/heap_sort_step9.png) 53 | 54 | === "<10>" 55 | ![heap_sort_step10](heap_sort.assets/heap_sort_step10.png) 56 | 57 | === "<11>" 58 | ![heap_sort_step11](heap_sort.assets/heap_sort_step11.png) 59 | 60 | === "<12>" 61 | ![heap_sort_step12](heap_sort.assets/heap_sort_step12.png) 62 | 63 | 在代码实现中,我们使用了与堆章节相同的从顶至底堆化 `sift_down()` 函数。值得注意的是,由于堆的长度会随着提取最大元素而减小,因此我们需要给 `sift_down()` 函数添加一个长度参数 $n$ ,用于指定堆的当前有效长度。 64 | 65 | ```src 66 | [file]{heap_sort}-[class]{}-[func]{heap_sort} 67 | ``` 68 | 69 | ## 算法特性 70 | 71 | - **时间复杂度 $O(n \log n)$、非自适应排序**:建堆操作使用 $O(n)$ 时间。从堆中提取最大元素的时间复杂度为 $O(\log n)$ ,共循环 $n - 1$ 轮。 72 | - **空间复杂度 $O(1)$、原地排序**:几个指针变量使用 $O(1)$ 空间。元素交换和堆化操作都是在原数组上进行的。 73 | - **非稳定排序**:在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化。 74 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/insertion_sort.md: -------------------------------------------------------------------------------- 1 | # 插入排序 2 | 3 | 「插入排序 insertion sort」是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。 4 | 5 | 具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。 6 | 7 | 下图展示了数组插入元素的操作流程。设基准元素为 `base` ,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。 8 | 9 | ![单次插入操作](insertion_sort.assets/insertion_operation.png) 10 | 11 | ## 算法流程 12 | 13 | 插入排序的整体流程如下图所示。 14 | 15 | 1. 初始状态下,数组的第 1 个元素已完成排序。 16 | 2. 选取数组的第 2 个元素作为 `base` ,将其插入到正确位置后,**数组的前 2 个元素已排序**。 17 | 3. 选取第 3 个元素作为 `base` ,将其插入到正确位置后,**数组的前 3 个元素已排序**。 18 | 4. 以此类推,在最后一轮中,选取最后一个元素作为 `base` ,将其插入到正确位置后,**所有元素均已排序**。 19 | 20 | ![插入排序流程](insertion_sort.assets/insertion_sort_overview.png) 21 | 22 | ```src 23 | [file]{insertion_sort}-[class]{}-[func]{insertion_sort} 24 | ``` 25 | 26 | ## 算法特性 27 | 28 | - **时间复杂度 $O(n^2)$、自适应排序**:最差情况下,每次插入操作分别需要循环 $n - 1$、$n-2$、$\dots$、$2$、$1$ 次,求和得到 $(n - 1) n / 2$ ,因此时间复杂度为 $O(n^2)$ 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 $O(n)$ 。 29 | - **空间复杂度 $O(1)$、原地排序**:指针 $i$ 和 $j$ 使用常数大小的额外空间。 30 | - **稳定排序**:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。 31 | 32 | ## 插入排序优势 33 | 34 | 插入排序的时间复杂度为 $O(n^2)$ ,而我们即将学习的快速排序的时间复杂度为 $O(n \log n)$ 。尽管插入排序的时间复杂度相比快速排序更高,**但在数据量较小的情况下,插入排序通常更快**。 35 | 36 | 这个结论与线性查找和二分查找的适用情况的结论类似。快速排序这类 $O(n \log n)$ 的算法属于基于分治的排序算法,往往包含更多单元计算操作。而在数据量较小时,$n^2$ 和 $n \log n$ 的数值比较接近,复杂度不占主导作用;每轮中的单元操作数量起到决定性因素。 37 | 38 | 实际上,许多编程语言(例如 Java)的内置排序函数都采用了插入排序,大致思路为:对于长数组,采用基于分治的排序算法,例如快速排序;对于短数组,直接使用插入排序。 39 | 40 | 虽然冒泡排序、选择排序和插入排序的时间复杂度都为 $O(n^2)$ ,但在实际情况中,**插入排序的使用频率显著高于冒泡排序和选择排序**,主要有以下原因。 41 | 42 | - 冒泡排序基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;插入排序基于元素赋值实现,仅需 1 个单元操作。因此,**冒泡排序的计算开销通常比插入排序更高**。 43 | - 选择排序在任何情况下的时间复杂度都为 $O(n^2)$ 。**如果给定一组部分有序的数据,插入排序通常比选择排序效率更高**。 44 | - 选择排序不稳定,无法应用于多级排序。 45 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/n_queens_problem.md: -------------------------------------------------------------------------------- 1 | # N 皇后问题 2 | 3 | !!! question 4 | 5 | 根据国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。给定 $n$ 个皇后和一个 $n \times n$ 大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案。 6 | 7 | 如下图所示,当 $n = 4$ 时,共可以找到两个解。从回溯算法的角度看,$n \times n$ 大小的棋盘共有 $n^2$ 个格子,给出了所有的选择 `choices` 。在逐个放置皇后的过程中,棋盘状态在不断地变化,每个时刻的棋盘就是状态 `state` 。 8 | 9 | ![4 皇后问题的解](n_queens_problem.assets/solution_4_queens.png) 10 | 11 | 下图展示了本题的三个约束条件:**多个皇后不能在同一行、同一列、同一对角线**。值得注意的是,对角线分为主对角线 `\` 和次对角线 `/` 两种。 12 | 13 | ![n 皇后问题的约束条件](n_queens_problem.assets/n_queens_constraints.png) 14 | 15 | ### 逐行放置策略 16 | 17 | 皇后的数量和棋盘的行数都为 $n$ ,因此我们容易得到一个推论:**棋盘每行都允许且只允许放置一个皇后**。 18 | 19 | 也就是说,我们可以采取逐行放置策略:从第一行开始,在每行放置一个皇后,直至最后一行结束。 20 | 21 | 如下图所示,为 $4$ 皇后问题的逐行放置过程。受画幅限制,下图仅展开了第一行的其中一个搜索分支,并且将不满足列约束和对角线约束的方案都进行了剪枝。 22 | 23 | ![逐行放置策略](n_queens_problem.assets/n_queens_placing.png) 24 | 25 | 本质上看,**逐行放置策略起到了剪枝的作用**,它避免了同一行出现多个皇后的所有搜索分支。 26 | 27 | ### 列与对角线剪枝 28 | 29 | 为了满足列约束,我们可以利用一个长度为 $n$ 的布尔型数组 `cols` 记录每一列是否有皇后。在每次决定放置前,我们通过 `cols` 将已有皇后的列进行剪枝,并在回溯中动态更新 `cols` 的状态。 30 | 31 | 那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 $(row, col)$ ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,**即对角线上所有格子的 $row - col$ 为恒定值**。 32 | 33 | 也就是说,如果两个格子满足 $row_1 - col_1 = row_2 - col_2$ ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助下图所示的数组 `diag1` ,记录每条主对角线上是否有皇后。 34 | 35 | 同理,**次对角线上的所有格子的 $row + col$ 是恒定值**。我们同样也可以借助数组 `diag2` 来处理次对角线约束。 36 | 37 | ![处理列约束和对角线约束](n_queens_problem.assets/n_queens_cols_diagonals.png) 38 | 39 | ### 代码实现 40 | 41 | 请注意,$n$ 维方阵中 $row - col$ 的范围是 $[-n + 1, n - 1]$ ,$row + col$ 的范围是 $[0, 2n - 2]$ ,所以主对角线和次对角线的数量都为 $2n - 1$ ,即数组 `diag1` 和 `diag2` 的长度都为 $2n - 1$ 。 42 | 43 | ```src 44 | [file]{n_queens}-[class]{}-[func]{n_queens} 45 | ``` 46 | 47 | 逐行放置 $n$ 次,考虑列约束,则从第一行到最后一行分别有 $n$、$n-1$、$\dots$、$2$、$1$ 个选择,**因此时间复杂度为 $O(n!)$** 。实际上,根据对角线约束的剪枝也能够大幅地缩小搜索空间,因而搜索效率往往优于以上时间复杂度。 48 | 49 | 数组 `state` 使用 $O(n^2)$ 空间,数组 `cols`、`diags1` 和 `diags2` 皆使用 $O(n)$ 空间。最大递归深度为 $n$ ,使用 $O(n)$ 栈帧空间。因此,**空间复杂度为 $O(n^2)$** 。 50 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/performance_evaluation.md: -------------------------------------------------------------------------------- 1 | # 算法效率评估 2 | 3 | 在算法设计中,我们先后追求以下两个层面的目标。 4 | 5 | 1. **找到问题解法**:算法需要在规定的输入范围内,可靠地求得问题的正确解。 6 | 2. **寻求最优解法**:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。 7 | 8 | 也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。 9 | 10 | - **时间效率**:算法运行速度的快慢。 11 | - **空间效率**:算法占用内存空间的大小。 12 | 13 | 简而言之,**我们的目标是设计“既快又省”的数据结构与算法**。而有效地评估算法效率至关重要,因为只有这样我们才能将各种算法进行对比,从而指导算法设计与优化过程。 14 | 15 | 效率评估方法主要分为两种:实际测试、理论估算。 16 | 17 | ## 实际测试 18 | 19 | 假设我们现在有算法 `A` 和算法 `B` ,它们都能解决同一问题,现在需要对比这两个算法的效率。最直接的方法是找一台计算机,运行这两个算法,并监控记录它们的运行时间和内存占用情况。这种评估方式能够反映真实情况,但也存在较大局限性。 20 | 21 | 一方面,**难以排除测试环境的干扰因素**。硬件配置会影响算法的性能表现。比如在某台计算机中,算法 `A` 的运行时间比算法 `B` 短;但在另一台配置不同的计算机中,我们可能得到相反的测试结果。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。 22 | 23 | 另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 更少;而输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这需要耗费大量的计算资源。 24 | 25 | ## 理论估算 26 | 27 | 由于实际测试具有较大的局限性,我们可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为「渐近复杂度分析 asymptotic complexity analysis」,简称「复杂度分析」。 28 | 29 | 复杂度分析体现算法运行所需的时间(空间)资源与输入数据大小之间的关系。**它描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势**。这个定义有些拗口,我们可以将其分为三个重点来理解。 30 | 31 | - “时间和空间资源”分别对应「时间复杂度 time complexity」和「空间复杂度 space complexity」。 32 | - “随着输入数据大小的增加”意味着复杂度反映了算法运行效率与输入数据体量之间的关系。 33 | - “时间和空间的增长趋势”表示复杂度分析关注的不是运行时间或占用空间的具体值,而是时间或空间增长的“快慢”。 34 | 35 | **复杂度分析克服了实际测试方法的弊端**,体现在以下两个方面。 36 | 37 | - 它独立于测试环境,分析结果适用于所有运行平台。 38 | - 它可以体现不同数据量下的算法效率,尤其是在大数据量下的算法性能。 39 | 40 | !!! tip 41 | 42 | 如果你仍对复杂度的概念感到困惑,无须担心,我们会在后续章节中详细介绍。 43 | 44 | 复杂度分析为我们提供了一把评估算法效率的“标尺”,使我们可以衡量执行某个算法所需的时间和空间资源,对比不同算法之间的效率。 45 | 46 | 复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度看,复杂度分析可能不太适合作为最先介绍的内容。然而,当我们讨论某个数据结构或算法的特点时,难以避免要分析其运行速度和空间使用情况。 47 | 48 | 综上所述,建议你在深入学习数据结构与算法之前,**先对复杂度分析建立初步的了解,以便能够完成简单算法的复杂度分析**。 49 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/radix_sort.md: -------------------------------------------------------------------------------- 1 | # 基数排序 2 | 3 | 上一节我们介绍了计数排序,它适用于数据量 $n$ 较大但数据范围 $m$ 较小的情况。假设我们需要对 $n = 10^6$ 个学号进行排序,而学号是一个 $8$ 位数字,这意味着数据范围 $m = 10^8$ 非常大,使用计数排序需要分配大量内存空间,而基数排序可以避免这种情况。 4 | 5 | 「基数排序 radix sort」的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。 6 | 7 | ## 算法流程 8 | 9 | 以学号数据为例,假设数字的最低位是第 $1$ 位,最高位是第 $8$ 位,基数排序的流程如下图所示。 10 | 11 | 1. 初始化位数 $k = 1$ 。 12 | 2. 对学号的第 $k$ 位执行“计数排序”。完成后,数据会根据第 $k$ 位从小到大排序。 13 | 3. 将 $k$ 增加 $1$ ,然后返回步骤 `2.` 继续迭代,直到所有位都排序完成后结束。 14 | 15 | ![基数排序算法流程](radix_sort.assets/radix_sort_overview.png) 16 | 17 | 下面来剖析代码实现。对于一个 $d$ 进制的数字 $x$ ,要获取其第 $k$ 位 $x_k$ ,可以使用以下计算公式: 18 | 19 | $$ 20 | x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d 21 | $$ 22 | 23 | 其中 $\lfloor a \rfloor$ 表示对浮点数 $a$ 向下取整,而 $\bmod \: d$ 表示对 $d$ 取余。对于学号数据,$d = 10$ 且 $k \in [1, 8]$ 。 24 | 25 | 此外,我们需要小幅改动计数排序代码,使之可以根据数字的第 $k$ 位进行排序。 26 | 27 | ```src 28 | [file]{radix_sort}-[class]{}-[func]{radix_sort} 29 | ``` 30 | 31 | !!! question "为什么从最低位开始排序?" 32 | 33 | 在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 $a < b$ ,而第二轮排序结果 $a > b$ ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,我们应该先排序低位再排序高位。 34 | 35 | ## 算法特性 36 | 37 | 相较于计数排序,基数排序适用于数值范围较大的情况,**但前提是数据必须可以表示为固定位数的格式,且位数不能过大**。例如,浮点数不适合使用基数排序,因为其位数 $k$ 过大,可能导致时间复杂度 $O(nk) \gg O(n^2)$ 。 38 | 39 | - **时间复杂度 $O(nk)$**:设数据量为 $n$、数据为 $d$ 进制、最大位数为 $k$ ,则对某一位执行计数排序使用 $O(n + d)$ 时间,排序所有 $k$ 位使用 $O((n + d)k)$ 时间。通常情况下,$d$ 和 $k$ 都相对较小,时间复杂度趋向 $O(n)$ 。 40 | - **空间复杂度 $O(n + d)$、非原地排序**:与计数排序相同,基数排序需要借助长度为 $n$ 和 $d$ 的数组 `res` 和 `counter` 。 41 | - **稳定排序**:与计数排序相同。 42 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/replace_linear_by_hashing.md: -------------------------------------------------------------------------------- 1 | # 哈希优化策略 2 | 3 | 在算法题中,**我们常通过将线性查找替换为哈希查找来降低算法的时间复杂度**。我们借助一个算法题来加深理解。 4 | 5 | !!! question 6 | 7 | 给定一个整数数组 `nums` 和一个目标元素 `target` ,请在数组中搜索“和”为 `target` 的两个元素,并返回它们的数组索引。返回任意一个解即可。 8 | 9 | ## 线性查找:以时间换空间 10 | 11 | 考虑直接遍历所有可能的组合。如下图所示,我们开启一个两层循环,在每轮中判断两个整数的和是否为 `target` ,若是则返回它们的索引。 12 | 13 | ![线性查找求解两数之和](replace_linear_by_hashing.assets/two_sum_brute_force.png) 14 | 15 | ```src 16 | [file]{two_sum}-[class]{}-[func]{two_sum_brute_force} 17 | ``` 18 | 19 | 此方法的时间复杂度为 $O(n^2)$ ,空间复杂度为 $O(1)$ ,在大数据量下非常耗时。 20 | 21 | ## 哈希查找:以空间换时间 22 | 23 | 考虑借助一个哈希表,键值对分别为数组元素和元素索引。循环遍历数组,每轮执行下图所示的步骤。 24 | 25 | 1. 判断数字 `target - nums[i]` 是否在哈希表中,若是则直接返回这两个元素的索引。 26 | 2. 将键值对 `nums[i]` 和索引 `i` 添加进哈希表。 27 | 28 | === "<1>" 29 | ![辅助哈希表求解两数之和](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png) 30 | 31 | === "<2>" 32 | ![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png) 33 | 34 | === "<3>" 35 | ![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png) 36 | 37 | 实现代码如下所示,仅需单层循环即可。 38 | 39 | ```src 40 | [file]{two_sum}-[class]{}-[func]{two_sum_hash_table} 41 | ``` 42 | 43 | 此方法通过哈希查找将时间复杂度从 $O(n^2)$ 降低至 $O(n)$ ,大幅提升运行效率。 44 | 45 | 由于需要维护一个额外的哈希表,因此空间复杂度为 $O(n)$ 。**尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法**。 46 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/selection_sort.md: -------------------------------------------------------------------------------- 1 | # 选择排序 2 | 3 | 「选择排序 selection sort」的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。 4 | 5 | 设数组的长度为 $n$ ,选择排序的算法流程如下图所示。 6 | 7 | 1. 初始状态下,所有元素未排序,即未排序(索引)区间为 $[0, n-1]$ 。 8 | 2. 选取区间 $[0, n-1]$ 中的最小元素,将其与索引 $0$ 处元素交换。完成后,数组前 1 个元素已排序。 9 | 3. 选取区间 $[1, n-1]$ 中的最小元素,将其与索引 $1$ 处元素交换。完成后,数组前 2 个元素已排序。 10 | 4. 以此类推。经过 $n - 1$ 轮选择与交换后,数组前 $n - 1$ 个元素已排序。 11 | 5. 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。 12 | 13 | === "<1>" 14 | ![选择排序步骤](selection_sort.assets/selection_sort_step1.png) 15 | 16 | === "<2>" 17 | ![selection_sort_step2](selection_sort.assets/selection_sort_step2.png) 18 | 19 | === "<3>" 20 | ![selection_sort_step3](selection_sort.assets/selection_sort_step3.png) 21 | 22 | === "<4>" 23 | ![selection_sort_step4](selection_sort.assets/selection_sort_step4.png) 24 | 25 | === "<5>" 26 | ![selection_sort_step5](selection_sort.assets/selection_sort_step5.png) 27 | 28 | === "<6>" 29 | ![selection_sort_step6](selection_sort.assets/selection_sort_step6.png) 30 | 31 | === "<7>" 32 | ![selection_sort_step7](selection_sort.assets/selection_sort_step7.png) 33 | 34 | === "<8>" 35 | ![selection_sort_step8](selection_sort.assets/selection_sort_step8.png) 36 | 37 | === "<9>" 38 | ![selection_sort_step9](selection_sort.assets/selection_sort_step9.png) 39 | 40 | === "<10>" 41 | ![selection_sort_step10](selection_sort.assets/selection_sort_step10.png) 42 | 43 | === "<11>" 44 | ![selection_sort_step11](selection_sort.assets/selection_sort_step11.png) 45 | 46 | 在代码中,我们用 $k$ 来记录未排序区间内的最小元素。 47 | 48 | ```src 49 | [file]{selection_sort}-[class]{}-[func]{selection_sort} 50 | ``` 51 | 52 | ## 算法特性 53 | 54 | - **时间复杂度为 $O(n^2)$、非自适应排序**:外循环共 $n - 1$ 轮,第一轮的未排序区间长度为 $n$ ,最后一轮的未排序区间长度为 $2$ ,即各轮外循环分别包含 $n$、$n - 1$、$\dots$、$3$、$2$ 轮内循环,求和为 $\frac{(n - 1)(n + 2)}{2}$ 。 55 | - **空间复杂度 $O(1)$、原地排序**:指针 $i$ 和 $j$ 使用常数大小的额外空间。 56 | - **非稳定排序**:如下图所示,元素 `nums[i]` 有可能被交换至与其相等的元素的右边,导致两者相对顺序发生改变。 57 | 58 | ![选择排序非稳定示例](selection_sort.assets/selection_sort_instability.png) 59 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/sorting_algorithm.md: -------------------------------------------------------------------------------- 1 | # 排序算法 2 | 3 | 「排序算法 sorting algorithm」用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更有效地查找、分析和处理。 4 | 5 | 如下图所示,排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。 6 | 7 | ![数据类型和判断规则示例](sorting_algorithm.assets/sorting_examples.png) 8 | 9 | ## 评价维度 10 | 11 | **运行效率**:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(即时间复杂度中的常数项降低)。对于大数据量情况,运行效率显得尤为重要。 12 | 13 | **就地性**:顾名思义,「原地排序」通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。 14 | 15 | **稳定性**:「稳定排序」在完成排序后,相等元素在数组中的相对顺序不发生改变。 16 | 17 | 稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,「非稳定排序」可能导致输入数据的有序性丧失。 18 | 19 | ```shell 20 | # 输入数据是按照姓名排序好的 21 | # (name, age) 22 | ('A', 19) 23 | ('B', 18) 24 | ('C', 21) 25 | ('D', 19) 26 | ('E', 23) 27 | 28 | # 假设使用非稳定排序算法按年龄排序列表, 29 | # 结果中 ('D', 19) 和 ('A', 19) 的相对位置改变, 30 | # 输入数据按姓名排序的性质丢失 31 | ('B', 18) 32 | ('D', 19) 33 | ('A', 19) 34 | ('C', 21) 35 | ('E', 23) 36 | ``` 37 | 38 | **自适应性**:「自适应排序」的时间复杂度会受输入数据的影响,即最佳、最差、平均时间复杂度并不完全相等。 39 | 40 | 自适应性需要根据具体情况来评估。如果最差时间复杂度差于平均时间复杂度,说明排序算法在某些数据下性能可能劣化,因此被视为负面属性;而如果最佳时间复杂度优于平均时间复杂度,则被视为正面属性。 41 | 42 | **是否基于比较**:「基于比较的排序」依赖于比较运算符($<$、$=$、$>$)来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 $O(n \log n)$ 。而「非比较排序」不使用比较运算符,时间复杂度可达 $O(n)$ ,但其通用性相对较差。 43 | 44 | ## 理想排序算法 45 | 46 | **运行快、原地、稳定、正向自适应、通用性好**。显然,迄今为止尚未发现兼具以上所有特性的排序算法。因此,在选择排序算法时,需要根据具体的数据特点和问题需求来决定。 47 | 48 | 接下来,我们将共同学习各种排序算法,并基于上述评价维度对各个排序算法的优缺点进行分析。 49 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/summary.md: -------------------------------------------------------------------------------- 1 | # 小结 2 | 3 | ### 重点回顾 4 | 5 | - 栈是一种遵循先入后出原则的数据结构,可通过数组或链表来实现。 6 | - 从时间效率角度看,栈的数组实现具有较高的平均效率,但在扩容过程中,单次入栈操作的时间复杂度会劣化至 $O(n)$ 。相比之下,基于链表实现的栈具有更为稳定的效率表现。 7 | - 在空间效率方面,栈的数组实现可能导致一定程度的空间浪费。但需要注意的是,链表节点所占用的内存空间比数组元素更大。 8 | - 队列是一种遵循先入先出原则的数据结构,同样可以通过数组或链表来实现。在时间效率和空间效率的对比上,队列的结论与前述栈的结论相似。 9 | - 双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。 10 | 11 | ### Q & A 12 | 13 | !!! question "浏览器的前进后退是否是双向链表实现?" 14 | 15 | 浏览器的前进后退功能本质上是“栈”的体现。当用户访问一个新页面时,该页面会被添加到栈顶;当用户点击后退按钮时,该页面会从栈顶弹出。使用双向队列可以方便实现一些额外操作,这个在双向队列章节有提到。 16 | 17 | !!! question "在出栈后,是否需要释放出栈节点的内存?" 18 | 19 | 如果后续仍需要使用弹出节点,则不需要释放内存。若之后不需要用到,`Java` 和 `Python` 等语言拥有自动垃圾回收机制,因此不需要手动释放内存;在 `C` 和 `C++` 中需要手动释放内存。 20 | 21 | !!! question "双向队列像是两个栈拼接在了一起,它的用途是什么?" 22 | 23 | 双向队列就像是栈和队列的组合,或者是两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。 24 | 25 | !!! question "撤销(undo)和反撤销(redo)具体是如何实现的?" 26 | 27 | 使用两个堆栈,栈 `A` 用于撤销,栈 `B` 用于反撤销。 28 | 29 | 1. 每当用户执行一个操作,将这个操作压入栈 `A` ,并清空栈 `B` 。 30 | 2. 当用户执行“撤销”时,从栈 `A` 中弹出最近的操作,并将其压入栈 `B` 。 31 | 3. 当用户执行“反撤销”时,从栈 `B` 中弹出最近的操作,并将其压入栈 `A` 。 32 | -------------------------------------------------------------------------------- /docs/question-bank/zh/programming/hello-algo/top_k.md: -------------------------------------------------------------------------------- 1 | # Top-K 问题 2 | 3 | !!! question 4 | 5 | 给定一个长度为 $n$ 无序数组 `nums` ,请返回数组中前 $k$ 大的元素。 6 | 7 | 对于该问题,我们先介绍两种思路比较直接的解法,再介绍效率更高的堆解法。 8 | 9 | ## 方法一:遍历选择 10 | 11 | 我们可以进行下图所示的 $k$ 轮遍历,分别在每轮中提取第 $1$、$2$、$\dots$、$k$ 大的元素,时间复杂度为 $O(nk)$ 。 12 | 13 | 此方法只适用于 $k \ll n$ 的情况,因为当 $k$ 与 $n$ 比较接近时,其时间复杂度趋向于 $O(n^2)$ ,非常耗时。 14 | 15 | ![遍历寻找最大的 k 个元素](top_k.assets/top_k_traversal.png) 16 | 17 | !!! tip 18 | 19 | 当 $k = n$ 时,我们可以得到完整的有序序列,此时等价于“选择排序”算法。 20 | 21 | ## 方法二:排序 22 | 23 | 如下图所示,我们可以先对数组 `nums` 进行排序,再返回最右边的 $k$ 个元素,时间复杂度为 $O(n \log n)$ 。 24 | 25 | 显然,该方法“超额”完成任务了,因为我们只需要找出最大的 $k$ 个元素即可,而不需要排序其他元素。 26 | 27 | ![排序寻找最大的 k 个元素](top_k.assets/top_k_sorting.png) 28 | 29 | ## 方法三:堆 30 | 31 | 我们可以基于堆更加高效地解决 Top-K 问题,流程如下图所示。 32 | 33 | 1. 初始化一个小顶堆,其堆顶元素最小。 34 | 2. 先将数组的前 $k$ 个元素依次入堆。 35 | 3. 从第 $k + 1$ 个元素开始,若当前元素大于堆顶元素,则将堆顶元素出堆,并将当前元素入堆。 36 | 4. 遍历完成后,堆中保存的就是最大的 $k$ 个元素。 37 | 38 | === "<1>" 39 | ![基于堆寻找最大的 k 个元素](top_k.assets/top_k_heap_step1.png) 40 | 41 | === "<2>" 42 | ![top_k_heap_step2](top_k.assets/top_k_heap_step2.png) 43 | 44 | === "<3>" 45 | ![top_k_heap_step3](top_k.assets/top_k_heap_step3.png) 46 | 47 | === "<4>" 48 | ![top_k_heap_step4](top_k.assets/top_k_heap_step4.png) 49 | 50 | === "<5>" 51 | ![top_k_heap_step5](top_k.assets/top_k_heap_step5.png) 52 | 53 | === "<6>" 54 | ![top_k_heap_step6](top_k.assets/top_k_heap_step6.png) 55 | 56 | === "<7>" 57 | ![top_k_heap_step7](top_k.assets/top_k_heap_step7.png) 58 | 59 | === "<8>" 60 | ![top_k_heap_step8](top_k.assets/top_k_heap_step8.png) 61 | 62 | === "<9>" 63 | ![top_k_heap_step9](top_k.assets/top_k_heap_step9.png) 64 | 65 | 总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。 66 | 67 | 另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大 $k$ 个元素的动态更新。 68 | 69 | ```src 70 | [file]{top_k}-[class]{}-[func]{top_k_heap} 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/screen-shot/en-export-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/en-export-import.png -------------------------------------------------------------------------------- /docs/screen-shot/en-question-type-answer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/en-question-type-answer.png -------------------------------------------------------------------------------- /docs/screen-shot/en-question-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/en-question-type.png -------------------------------------------------------------------------------- /docs/screen-shot/en-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/en-role.png -------------------------------------------------------------------------------- /docs/screen-shot/role-emoji-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/role-emoji-en.png -------------------------------------------------------------------------------- /docs/screen-shot/role-emoji-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/role-emoji-zh.png -------------------------------------------------------------------------------- /docs/screen-shot/zh-export-import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/zh-export-import.png -------------------------------------------------------------------------------- /docs/screen-shot/zh-question-type-answer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/zh-question-type-answer.png -------------------------------------------------------------------------------- /docs/screen-shot/zh-question-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/zh-question-type.png -------------------------------------------------------------------------------- /docs/screen-shot/zh-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/screen-shot/zh-role.png -------------------------------------------------------------------------------- /docs/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/social.png -------------------------------------------------------------------------------- /docs/usecase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/docs/usecase.gif -------------------------------------------------------------------------------- /next/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | /public/temp 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | .yarn/install-state.gz 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | -------------------------------------------------------------------------------- /next/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "*" 8 | 9 | [requires] 10 | python_version = "3.9" -------------------------------------------------------------------------------- /next/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### Install 6 | 7 | ```bash 8 | pnpm install 9 | ``` 10 | 11 | ### Run 12 | 13 | ```bash 14 | pnpm dev 15 | ``` 16 | 17 | ## Dependencies 18 | 19 | - UI Framework: [shadcn/ui](https://ui.shadcn.com/docs/components/accordion) 20 | 21 | - State Management: [zustand](https://docs.pmnd.rs/zustand/getting-started/introduction) 22 | 23 | - ORM: [prisma](https://www.prisma.io/docs/getting-started) 24 | 25 | - Form Validation: [react-hook-form](https://react-hook-form.com/get-started) 26 | 27 | - AIGC Framework: [langchain.js](https://js.langchain.com/docs/get_started/introduction) 28 | 29 | 30 | -------------------------------------------------------------------------------- /next/app/add-new/page.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@/components/share/header' 2 | import { UploadForm } from '@/components/form/upload-form' 3 | 4 | const AddNew = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | 16 | export default AddNew 17 | -------------------------------------------------------------------------------- /next/app/api/file/list/route.ts: -------------------------------------------------------------------------------- 1 | import { fileHandler } from '@/lib/db-handler' 2 | import { NextResponse } from 'next/server' 3 | 4 | export const GET = async (req: Request) => { 5 | try { 6 | const urlQuery = new URLSearchParams(req.url?.split('?')[1]) 7 | const noteId = urlQuery.get('noteId') || '' 8 | const files = await fileHandler.getFilesByNoteId(noteId) 9 | 10 | return new NextResponse( 11 | JSON.stringify({ 12 | files, 13 | }) 14 | ) 15 | } catch (error) { 16 | console.log('[Examor GET] Error: ', error) 17 | return new NextResponse(error as string, { status: 500 }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /next/app/api/file/uploading/route.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from 'ws' 2 | import { fileHandler } from '@/lib/db-handler' 3 | 4 | export const GET = async () => { 5 | const ws = new WebSocketServer({ port: 51782 }, () => { 6 | console.log('WebSocket server is running on port 51782') 7 | }) 8 | 9 | ws.on('error', (error) => { 10 | if (process.env.NODE_ENV !== 'development') 11 | console.log('WebSocket server failed to start, ' + error) 12 | return new Response('WebSocket server failed to start', { status: 500 }) 13 | }) 14 | 15 | ws.on('connection', (socket) => { 16 | const timer = setInterval(async () => { 17 | const files = await fileHandler.findUploading() 18 | socket.send(JSON.stringify(files)) 19 | }, 1000) 20 | 21 | socket.on('close', () => { 22 | clearInterval(timer) 23 | }) 24 | }) 25 | 26 | return new Response('success') 27 | } 28 | -------------------------------------------------------------------------------- /next/app/api/note/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { noteHandler } from '@/lib/db-handler' 2 | import { TNote } from '@prisma/client' 3 | import { NextRequest, NextResponse } from 'next/server' 4 | 5 | export const PATCH = async (req: NextRequest) => { 6 | try { 7 | const id = req.nextUrl.pathname.split('/').pop() as string 8 | const body: TNote = await req.json() 9 | const res = await noteHandler.update(id, body) 10 | return new NextResponse(JSON.stringify(res)) 11 | } catch (error) { 12 | console.log('[Examor PATCH] Error: ', error) 13 | return new NextResponse(error as string, { status: 500 }) 14 | } 15 | } 16 | 17 | export const DELETE = async (req: NextRequest) => { 18 | try { 19 | const id = req.nextUrl.pathname.split('/').pop() as string 20 | const res = await noteHandler.deleteNote(id) 21 | return new NextResponse(JSON.stringify(res)) 22 | } catch (error) { 23 | console.log('[Examor DELETE] Error: ', error) 24 | return new NextResponse(error as string, { status: 500 }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /next/app/api/note/all/route.ts: -------------------------------------------------------------------------------- 1 | import { noteHandler } from '@/lib/db-handler' 2 | import { NextResponse } from 'next/server' 3 | 4 | export const GET = async () => { 5 | try { 6 | const notes = await noteHandler.getAll() 7 | return new NextResponse(JSON.stringify(notes)) 8 | } catch (error) { 9 | console.log('[Examor GET] Error: ', error) 10 | return new NextResponse(error as string, { status: 500 }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /next/app/api/profile/init/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { profileHandler } from '@/lib/db-handler' 3 | 4 | export async function POST() { 5 | try { 6 | const profile = await profileHandler.init() 7 | return NextResponse.json(profile) 8 | } catch (error) { 9 | console.log('[Examor POST] Error: ', error) 10 | return new NextResponse('Internal Error', { status: 500 }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /next/app/api/profile/update/route.ts: -------------------------------------------------------------------------------- 1 | import { ProfileType } from '@/types/global' 2 | import { NextResponse } from 'next/server' 3 | import { profileHandler } from '@/lib/db-handler/index' 4 | 5 | export const PATCH = async (req: Request) => { 6 | try { 7 | const body: ProfileType = await req.json() 8 | const profile = await profileHandler.update(body) 9 | 10 | return NextResponse.json(profile) 11 | } catch (err) { 12 | console.log('[Examor PATCH] Error: ', err) 13 | return new NextResponse(err as string, { status: 500 }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /next/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeacme17/examor/51de0b7ce1c65ea46f622b1b14e8dd166a790f33/next/app/favicon.ico -------------------------------------------------------------------------------- /next/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Poppins } from 'next/font/google' 2 | import { cn } from '@/lib/utils' 3 | import { profileHandler } from '@/lib/db-handler' 4 | import type { Metadata } from 'next' 5 | import './globals.css' 6 | 7 | import NextTopLoader from 'nextjs-toploader' 8 | import { ThemeProvider } from '@/components/theme-provider' 9 | import { ResizePanel } from '@/components/layout/resize-panel' 10 | import { Toaster } from '@/components/ui/toaster' 11 | 12 | const poppins = Poppins({ 13 | subsets: ['latin-ext'], 14 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], 15 | fallback: ['system-ui', 'sans-serif'], 16 | }) 17 | 18 | export const metadata: Metadata = { 19 | title: 'examor | self-improvement', 20 | description: 21 | 'For students, scholars, interviewees and lifelong learners. Let LLMs assist you in learning 🎓', 22 | } 23 | 24 | export default async function RootLayout({ 25 | children, 26 | }: Readonly<{ children: React.ReactNode }>) { 27 | await profileHandler.init() 28 | 29 | return ( 30 | 31 | 32 | 36 | 37 | 38 | 39 | 46 | 47 | 52 | {children} 53 | 54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /next/app/manage-notes/_components/add-file-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useContext } from 'react' 2 | import { NoteContext } from '../_context/note-context' 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from '@/components/ui/dialog' 10 | import { Button } from '@/components/ui/button' 11 | import { Paperclip } from 'lucide-react' 12 | import { UploadForm } from '@/components/form/upload-form' 13 | 14 | interface AddFileDialogProps {} 15 | 16 | export const AddFileDialog = memo( 17 | (props: AddFileDialogProps) => { 18 | const noteContext = useContext(NoteContext) 19 | 20 | if (!noteContext?.note.id) return null 21 | 22 | const note = noteContext.note 23 | 24 | return ( 25 | 26 | 27 | 31 | 32 | 33 | 34 | Add new file to {note.name} 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | ) 43 | 44 | AddFileDialog.displayName = 'AddFileDialog' 45 | -------------------------------------------------------------------------------- /next/app/manage-notes/_components/file-manager.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useContext } from 'react' 2 | import { NoteHeader } from './note-header' 3 | import { NoteContext } from '../_context/note-context' 4 | import { FileTable } from './file-table' 5 | 6 | interface FileManagerProps { 7 | noteId: string 8 | } 9 | 10 | export const FileManager = memo((props: FileManagerProps) => { 11 | const { noteId } = props 12 | 13 | const noteContext = useContext(NoteContext) 14 | 15 | if (!noteContext?.note) return null 16 | 17 | return ( 18 |
19 | 20 | 21 |
22 | ) 23 | }) 24 | 25 | FileManager.displayName = 'FileManager' 26 | -------------------------------------------------------------------------------- /next/app/manage-notes/_components/note-header.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useContext } from 'react' 2 | import { NoteContext } from '../_context/note-context' 3 | import { useUploadingNotes } from '@/hooks/useUploadingNote' 4 | 5 | import { ArrowLeftCircle } from 'lucide-react' 6 | import { AddFileDialog } from './add-file-dialog' 7 | import { NoteIconPopover } from './note-icon-popover' 8 | import { DeletePopover } from './delete-popover' 9 | import { Button } from '@/components/ui/button' 10 | 11 | export const NoteHeader = memo(() => { 12 | const noteContext = useContext(NoteContext) 13 | 14 | const note = noteContext!.note 15 | const onBack = noteContext!.onBack 16 | 17 | const { isNoteUploading } = useUploadingNotes(note.id) 18 | 19 | return ( 20 |
21 |
22 | 30 | 31 | 32 | 33 | {note.name} 34 |
35 | 36 |
37 | 38 | 39 | 40 |
41 |
42 | ) 43 | }) 44 | 45 | NoteHeader.displayName = 'NoteHeader' 46 | -------------------------------------------------------------------------------- /next/app/manage-notes/_context/note-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | import { TNote } from '@prisma/client' 3 | 4 | export interface NoteContextProps { 5 | note: TNote 6 | setNote: (note: TNote) => void 7 | onBack: () => void 8 | changeIcon: (icon: string) => void 9 | } 10 | 11 | export const NoteContext = createContext(null) 12 | export const NoteContextProvider = NoteContext.Provider 13 | -------------------------------------------------------------------------------- /next/app/note/[id]/_components/note-header.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react' 2 | import { MdiIcon } from '@/components/mdi-icon' 3 | import type { TNote } from '@prisma/client' 4 | 5 | interface NoteHeader extends React.HTMLAttributes { 6 | note: TNote 7 | } 8 | 9 | export const NoteHeader = memo((props: NoteHeader) => { 10 | const { note, ...rest } = props 11 | 12 | return ( 13 |
14 | 15 | {note.name} 16 |
17 | ) 18 | }) 19 | 20 | NoteHeader.displayName = 'NoteHeader' 21 | -------------------------------------------------------------------------------- /next/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return null 3 | } 4 | -------------------------------------------------------------------------------- /next/app/profile/_components/anthropic-config-form.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { UseFormReturn } from 'react-hook-form' 3 | import { profileFormSchema as formSchema } from '@/schema/profile' 4 | 5 | import { 6 | FormControl, 7 | FormDescription, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from '@/components/ui/form' 13 | import { Input } from '@/components/ui/input' 14 | import { PasswordInput } from '@/components/share/password-input' 15 | 16 | interface AnthropicConfigFormProps { 17 | form: UseFormReturn> 18 | } 19 | 20 | export const AnthropicConfigForm = (props: AnthropicConfigFormProps) => { 21 | const { form } = props 22 | 23 | return ( 24 | <> 25 | ( 29 | 30 | Anthropic key 31 | 32 | 33 | 34 | 35 | You can find your key in the{' '} 36 | 40 | Anthropic Console 41 | 42 | 43 | 44 | 45 | )} 46 | /> 47 | 48 | ( 52 | 53 | Model name 54 | 55 | 56 | 57 | 58 | 59 | )} 60 | /> 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /next/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@/components/share/header' 2 | import { ProfileForm } from './_components/profile-form' 3 | 4 | const Profile = () => { 5 | return ( 6 |
7 |
11 | 12 | 13 |
14 | ) 15 | } 16 | 17 | export default Profile 18 | -------------------------------------------------------------------------------- /next/app/random-pick/page.tsx: -------------------------------------------------------------------------------- 1 | import { QABlock } from '@/components/qa-block' 2 | import { Question } from '@/types/global' 3 | 4 | const question: Question = { 5 | id: '1', 6 | question: 'What is your name?', 7 | answer: 'My name is John Doe', 8 | status: 'New', 9 | createdDate: '2024-02-02', 10 | updatedDate: '2024-02-02', 11 | questionType: 'short', 12 | roleType: 'examiner', 13 | } 14 | 15 | const RandomPick = () => { 16 | return ( 17 |
18 | 19 |
20 | ) 21 | } 22 | 23 | export default RandomPick 24 | -------------------------------------------------------------------------------- /next/app/template.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion } from 'framer-motion' 4 | 5 | export default function Template({ children }: { children: React.ReactNode }) { 6 | return ( 7 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /next/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next/components/layout/main.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useRef } from 'react' 4 | import { useProfileStore, useFileStore } from '@/store' 5 | import { useFetchNotes } from '@/hooks/useFetchNotes' 6 | 7 | export const Main = ({ children }: Readonly<{ children: React.ReactNode }>) => { 8 | const profileStore = useProfileStore() 9 | const fileStore = useFileStore() 10 | 11 | const ws = useRef(null) 12 | 13 | const { fetchNotes } = useFetchNotes() 14 | 15 | const fetchProfile = async () => { 16 | const res = await fetch('/api/profile/init', { 17 | method: 'POST', 18 | }) 19 | 20 | if (res.ok) { 21 | const data = await res.json() 22 | profileStore.setProfile(data) 23 | } else { 24 | console.log('Failed to fetch profile') 25 | } 26 | } 27 | 28 | const getUploadingFiles = async () => { 29 | if (ws.current) return 30 | 31 | await fetch('/api/file/uploading', { 32 | method: 'GET', 33 | }) 34 | 35 | ws.current = new WebSocket('ws://localhost:51782/') 36 | 37 | ws.current.onerror = (err) => { 38 | console.error(err) 39 | ws.current?.close() 40 | } 41 | 42 | ws.current.onmessage = (data) => { 43 | const uploadingFiles = JSON.parse(data.data) 44 | fileStore.setUploadingFiles(uploadingFiles) 45 | } 46 | } 47 | 48 | useEffect(() => { 49 | fetchProfile() 50 | fetchNotes() 51 | getUploadingFiles() 52 | 53 | return () => { 54 | ws.current?.close() 55 | } 56 | // eslint-disable-next-line react-hooks/exhaustive-deps 57 | }, []) 58 | 59 | return ( 60 |
{children}
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /next/components/layout/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter } from 'next/navigation' 4 | import { Button } from '@/components/ui/button' 5 | import { ChevronLeft, Settings } from 'lucide-react' 6 | import { ModeToggle } from './mode-toggle' 7 | import { Menubar } from './menu-bar' 8 | import { UploadingPopup } from '@/components/share/uploading-popup' 9 | 10 | export const Navbar = () => { 11 | const router = useRouter() 12 | 13 | return ( 14 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /next/components/layout/navbar/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useTheme } from 'next-themes' 5 | import { useHasMounted } from '@/hooks/useHasMouted' 6 | import { Button } from '@/components/ui/button' 7 | 8 | import { Moon, Sun } from 'lucide-react' 9 | 10 | export function ModeToggle() { 11 | const { setTheme, theme } = useTheme() 12 | 13 | const toggleTheme = () => { 14 | setTheme(theme === 'dark' ? 'light' : 'dark') 15 | } 16 | 17 | const hasMounted = useHasMounted() 18 | 19 | return ( 20 |
21 | {hasMounted && theme === 'dark' ? ( 22 | 25 | ) : ( 26 | 29 | )} 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /next/components/layout/resize-panel.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { cn } from '@/lib/utils' 5 | import { Navbar } from '@/components/layout/navbar' 6 | import { Sidebar } from '@/components/layout/sidebar' 7 | import { 8 | ResizableHandle, 9 | ResizablePanel, 10 | ResizablePanelGroup, 11 | } from '@/components/ui/resizable' 12 | import { useHasMounted } from '@/hooks/useHasMouted' 13 | import { Main } from './main' 14 | 15 | export const ResizePanel = ({ 16 | children, 17 | }: Readonly<{ children: React.ReactNode }>) => { 18 | const mounted = useHasMounted() 19 | 20 | const [isCollapsed, setIsCollapsed] = useState(false) 21 | const [isDragging, setIsDragging] = useState(false) 22 | 23 | if (!mounted) return null 24 | 25 | return ( 26 | 30 | { 37 | setIsCollapsed(true) 38 | }} 39 | onExpand={() => { 40 | setIsCollapsed(false) 41 | }} 42 | className={cn( 43 | 'hidden lg:block', 44 | isCollapsed && 45 | 'md:min-w-[70px] transition-all duration-300 ease-in-out', 46 | !isCollapsed && 'md:min-w-[250px]' 47 | )}> 48 | 49 | 50 | 51 | 59 | 60 | 61 |
62 | 63 | 64 |
{children}
65 |
66 |
67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /next/components/layout/sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { usePathname, useRouter } from 'next/navigation' 4 | import { cn } from '@/lib/utils' 5 | import { Separator } from '@/components/ui/separator' 6 | import { Button } from '@/components/ui/button' 7 | import { Plus } from 'lucide-react' 8 | import { MenuList } from './menu-list' 9 | import { Logo } from './logo' 10 | import { useMenu } from '@/hooks/useMenu' 11 | import { Skeleton } from '@/components/ui/skeleton' 12 | import { useNoteStore } from '@/store' 13 | 14 | export const Sidebar = ({ isCollapsed }: { isCollapsed: boolean }) => { 15 | const { staticMenus, noteMenus } = useMenu() 16 | const router = useRouter() 17 | const pathname = usePathname() 18 | const noteStore = useNoteStore() 19 | 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 52 | 53 | {noteStore.isFetching ? ( 54 | 55 | ) : ( 56 | 57 | )} 58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /next/components/layout/sidebar/logo.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect, useState } from 'react' 4 | import { useTheme } from 'next-themes' 5 | import Image from 'next/image' 6 | 7 | interface LogoProps { 8 | isCollapsed: boolean 9 | } 10 | 11 | export const Logo = ({ isCollapsed }: LogoProps) => { 12 | const { theme } = useTheme() 13 | 14 | const [logoSrc, setLogoSrc] = useState('/images/logo.svg') 15 | 16 | useEffect(() => { 17 | setLogoSrc(theme === 'dark' ? '/images/logo-dark.svg' : '/images/logo.svg') 18 | }, [theme]) 19 | 20 | return ( 21 |
22 | logo 30 | 31 | {!isCollapsed && ( 32 |
33 | examor 34 | 35 | self-improvement 36 | 37 |
38 | )} 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /next/components/mdi-icon.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | interface MdiIconProps extends React.HTMLAttributes { 4 | icon: string 5 | size?: string 6 | color?: string 7 | } 8 | 9 | export const MdiIcon = (props: MdiIconProps) => { 10 | const { 11 | icon, 12 | size = '1.3rem', 13 | color = 'currentColor', 14 | style, 15 | className, 16 | ...rest 17 | } = props 18 | 19 | return ( 20 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /next/components/qa-block/answer-block.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tabs, 3 | TabsContent, 4 | TabsList, 5 | TabsTrigger, 6 | } from '@/components/ui/tabs' 7 | import { Textarea } from '@/components/ui/textarea' 8 | import { Button } from '@/components/ui/button' 9 | import { FileClock, NotebookText } from 'lucide-react' 10 | import { ExamineBlock } from './examine-block' 11 | import { DocContent } from './doc-content' 12 | import { LastRecord } from './last-record' 13 | import './markdown.scss' 14 | 15 | export const AnswerBlock = () => { 16 | return ( 17 | 18 | 19 | Answer 20 | 21 | Last Record 22 | 23 | 24 | 25 | Note Content 26 | 27 | 28 | 29 | 30 |