├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── assets ├── AugmentedCanvas-AIgeneratedquestions.gif ├── AugmentedCanvas-AskAI.gif ├── AugmentedCanvas-AskquestionwithAI.gif ├── AugmentedCanvas-Createflashcards.gif └── AugmentedCanvas-Insertsystemprompt.gif ├── esbuild.config.mjs ├── manifest.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── src ├── AugmentedCanvasPlugin.ts ├── Modals │ ├── CustomQuestionModal.ts │ ├── FolderSuggestModal.ts │ ├── InputModal.ts │ └── SystemPromptsModal.ts ├── actions │ ├── canvasContextMenuActions │ │ └── flashcards.ts │ ├── canvasNodeContextMenuActions │ │ ├── flashcards.ts │ │ └── generateImage.ts │ ├── canvasNodeMenuActions │ │ ├── advancedCanvas.ts │ │ └── noteGenerator.ts │ ├── commands │ │ ├── insertSystemPrompt.ts │ │ ├── relevantQuestions.ts │ │ ├── runPromptFolder.ts │ │ ├── websiteContent.ts │ │ └── youtubeCaptions.ts │ └── menuPatches │ │ ├── noteMenuPatch.ts │ │ └── utils.ts ├── data │ └── prompts.csv.txt ├── logDebug.ts ├── obsidian │ ├── canvas-internal.d.ts │ ├── canvas-patches.ts │ ├── canvasUtil.ts │ ├── fileUtil.ts │ └── imageUtils.ts ├── openai │ └── models.ts ├── settings │ ├── AugmentedCanvasSettings.ts │ └── SettingsTab.ts ├── types │ ├── canvas.d.ts │ ├── custom.d.ts │ ├── event.d.ts │ └── obsidian.d.ts ├── utils.ts └── utils │ ├── chatgpt.ts │ ├── csvUtils.ts │ └── websiteContentUtils.ts ├── styles.css ├── tsconfig.json ├── version-bump.mjs ├── versions.json ├── yarn-error.log └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Léopold Szabatura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Augmented Canvas 2 | 3 | A plugin for [Obsidian](https://obsidian.md) that "augments" Obsidian Canvas with AI features. 4 | 5 | You need a OpenAI API Key to use this plugin, you can input it in the settings. The plugin only works with OpenAI latest model : `gpt-4-1106-preview` 6 | 7 | ## Key Features 8 | 9 | This plugin adds three actions to the Menu of a note in the Canvas View. 10 | 11 | 1. Ask GPT on a specific note, the note content will be used as prompt. The note can be a text note, a md file or a PDF file. A new note will be created underneath the prompt note containing the AI response. 12 | 13 | ![Augmented-Canvas-AskAI](./assets/AugmentedCanvas-AskAI.gif) 14 | 15 | 2. Ask question about a note. Also makes GPT generate a new note, the question is placed on the link between the two notes. 16 | 17 | ![Augmented-Canvas-AskquestionswithAI](./assets/AugmentedCanvas-AskquestionwithAI.gif) 18 | 19 | 3. Generate questions on a specific note using GPT. The generated questions help you easily dig further into the subject of the note. 20 | 21 | ![Augmented-Canvas-AIgeneratedquestions](./assets/AugmentedCanvas-AIgeneratedquestions.gif) 22 | 23 | The links between notes are used to create the chat history sent to GPT. 24 | 25 | ## Additional Features 26 | 27 | - The plugin adds an action to create an image in the context menu of a note in the canvas. 28 | 29 | - The plugin adds a command named "Run a system prompt on a folder". Reading all md and canvas files present in that folder and sub-folders and inserting the response in the current canvas. 30 | 31 | - The plugin adds a command named "Insert system prompt". This command will insert a chosen system prompt to the current canvas. The system prompts are fetch from [f/awesome-chatgpt-prompts (github.com)](https://github.com/f/awesome-chatgpt-prompts). You can also add your own system prompts in the settings. 32 | 33 | ![Augmented-Canvas-Insertsystemprompt](./assets/AugmentedCanvas-Insertsystemprompt.gif) 34 | 35 | - The plugin can create flashcards for you which can be revised using the [Spaced Repetition plugin](https://github.com/st3v3nmw/obsidian-spaced-repetition). Right click on a note to create flashcards. Then wait for GPT response and a new file will be created inside the folder specified in the settings. You can then revise this specific deck. Think about activating "Convert folders to decks and subdecks?" option in the settings of the Spaced Repetition plugin. 36 | 37 | ![Augmented-Canvas-Createflashcards](./assets/AugmentedCanvas-Createflashcards.gif) 38 | 39 | - The plugin adds a command named "Insert relevant questions". This command insert AI generated questions to the current canvas. The plugin reads and then sends your historical activity to GPT, reading the last X files modified (configurable in the settings). 40 | 41 | - The plugin adds an action to the edge context menu to regenerate an AI response. 42 | 43 | ## Privacy 44 | 45 | The content that is send to GPT can be viewed by toggling on the "Debug output" setting. The messages then appear in the console. 46 | 47 | ## Installation 48 | 49 | - Not ready for market yet 50 | - Can be installed via the [Brat](https://github.com/TfTHacker/obsidian42-brat) plugin 51 | You can see how to do so in this Ric Raftis article: https://ricraftis.au/obsidian/installing-the-brat-plugin-in-obsidian-a-step-by-step-guide/ 52 | - Manual installation 53 | 54 | 1. Find the release page on this github page and click 55 | 2. Download the latest release zip file 56 | 3. Unzip it, copy the unzipped folder to the obsidian plugin folder, make sure there are main.js and manifest.json files 57 | in the folder 58 | 4. Restart obsidian (do not restart also, you have to refresh plugin list), in the settings interface to enable the 59 | plugin 60 | 5. Done! 61 | 62 | ## Credits 63 | 64 | - [rpggio/obsidian-chat-stream: Obsidian canvas plugin for using AI completion with threads of canvas nodes (github.com)](https://github.com/rpggio/obsidian-chat-stream) 65 | - [Quorafind/Obsidian-Collapse-Node: A node collapsing plugin for Canvas in Obsidian. (github.com)](https://github.com/quorafind/obsidian-collapse-node) 66 | 67 | ## Support 68 | 69 | If you are enjoying this plugin then please support my work and enthusiasm by buying me a coffee 70 | on [https://www.buymeacoffee.com/metacorp](https://www.buymeacoffee.com/metacorp). 71 | . 72 | 73 | 74 | -------------------------------------------------------------------------------- /assets/AugmentedCanvas-AIgeneratedquestions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaCorp/obsidian-augmented-canvas/ee63226309e99cb22de0b5ee1b033c1750e8b2cf/assets/AugmentedCanvas-AIgeneratedquestions.gif -------------------------------------------------------------------------------- /assets/AugmentedCanvas-AskAI.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaCorp/obsidian-augmented-canvas/ee63226309e99cb22de0b5ee1b033c1750e8b2cf/assets/AugmentedCanvas-AskAI.gif -------------------------------------------------------------------------------- /assets/AugmentedCanvas-AskquestionwithAI.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaCorp/obsidian-augmented-canvas/ee63226309e99cb22de0b5ee1b033c1750e8b2cf/assets/AugmentedCanvas-AskquestionwithAI.gif -------------------------------------------------------------------------------- /assets/AugmentedCanvas-Createflashcards.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaCorp/obsidian-augmented-canvas/ee63226309e99cb22de0b5ee1b033c1750e8b2cf/assets/AugmentedCanvas-Createflashcards.gif -------------------------------------------------------------------------------- /assets/AugmentedCanvas-Insertsystemprompt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaCorp/obsidian-augmented-canvas/ee63226309e99cb22de0b5ee1b033c1750e8b2cf/assets/AugmentedCanvas-Insertsystemprompt.gif -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/AugmentedCanvasPlugin.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins, 35 | ], 36 | format: "cjs", 37 | watch: !prod, 38 | target: "es2018", 39 | logLevel: "info", 40 | sourcemap: prod ? false : "inline", 41 | treeShaking: true, 42 | outfile: "main.js", 43 | }) 44 | .catch(() => process.exit(1)); 45 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "augmented-canvas", 3 | "name": "Augmented Canvas", 4 | "version": "0.1.20", 5 | "minAppVersion": "1.1.0", 6 | "description": "Obsidian Canvas with AI features.", 7 | "author": "MetaCorp", 8 | "authorUrl": "https://github.com/MetaCorp", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://www.buymeacoffee.com/metacorp" 11 | }, 12 | "isDesktopOnly": false 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-augmented-canvas", 3 | "version": "0.1.16", 4 | "description": "Obsidian Canvas with AI features.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@typescript-eslint/eslint-plugin": "5.29.0", 17 | "@typescript-eslint/parser": "5.29.0", 18 | "builtin-modules": "3.3.0", 19 | "esbuild": "0.14.47", 20 | "obsidian": "latest", 21 | "tslib": "2.4.0", 22 | "typescript": "4.7.4" 23 | }, 24 | "dependencies": { 25 | "fuse.js": "^7.0.0", 26 | "js-tiktoken": "^1.0.8", 27 | "monkey-around": "^2.3.0", 28 | "openai": "^4.25.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@types/lodash': 9 | specifier: ^4.14.197 10 | version: 4.14.197 11 | lodash: 12 | specifier: ^4.17.21 13 | version: 4.17.21 14 | monkey-around: 15 | specifier: ^2.3.0 16 | version: 2.3.0 17 | 18 | devDependencies: 19 | '@types/node': 20 | specifier: ^16.11.6 21 | version: 16.18.46 22 | '@typescript-eslint/eslint-plugin': 23 | specifier: 5.29.0 24 | version: 5.29.0(@typescript-eslint/parser@5.29.0)(eslint@8.48.0)(typescript@4.7.4) 25 | '@typescript-eslint/parser': 26 | specifier: 5.29.0 27 | version: 5.29.0(eslint@8.48.0)(typescript@4.7.4) 28 | builtin-modules: 29 | specifier: 3.3.0 30 | version: 3.3.0 31 | esbuild: 32 | specifier: 0.14.47 33 | version: 0.14.47 34 | obsidian: 35 | specifier: latest 36 | version: 1.4.4(@codemirror/state@6.2.1)(@codemirror/view@6.16.0) 37 | tslib: 38 | specifier: 2.4.0 39 | version: 2.4.0 40 | typescript: 41 | specifier: 4.7.4 42 | version: 4.7.4 43 | 44 | packages: 45 | 46 | /@aashutoshrathi/word-wrap@1.2.6: 47 | resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} 48 | engines: {node: '>=0.10.0'} 49 | dev: true 50 | 51 | /@codemirror/state@6.2.1: 52 | resolution: {integrity: sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==} 53 | dev: true 54 | 55 | /@codemirror/view@6.16.0: 56 | resolution: {integrity: sha512-1Z2HkvkC3KR/oEZVuW9Ivmp8TWLzGEd8T8TA04TTwPvqogfkHBdYSlflytDOqmkUxM2d1ywTg7X2dU5mC+SXvg==} 57 | dependencies: 58 | '@codemirror/state': 6.2.1 59 | style-mod: 4.1.0 60 | w3c-keyname: 2.2.8 61 | dev: true 62 | 63 | /@eslint-community/eslint-utils@4.4.0(eslint@8.48.0): 64 | resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} 65 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 66 | peerDependencies: 67 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 68 | dependencies: 69 | eslint: 8.48.0 70 | eslint-visitor-keys: 3.4.3 71 | dev: true 72 | 73 | /@eslint-community/regexpp@4.8.0: 74 | resolution: {integrity: sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==} 75 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 76 | dev: true 77 | 78 | /@eslint/eslintrc@2.1.2: 79 | resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} 80 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 81 | dependencies: 82 | ajv: 6.12.6 83 | debug: 4.3.4 84 | espree: 9.6.1 85 | globals: 13.21.0 86 | ignore: 5.2.4 87 | import-fresh: 3.3.0 88 | js-yaml: 4.1.0 89 | minimatch: 3.1.2 90 | strip-json-comments: 3.1.1 91 | transitivePeerDependencies: 92 | - supports-color 93 | dev: true 94 | 95 | /@eslint/js@8.48.0: 96 | resolution: {integrity: sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==} 97 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 98 | dev: true 99 | 100 | /@humanwhocodes/config-array@0.11.10: 101 | resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} 102 | engines: {node: '>=10.10.0'} 103 | dependencies: 104 | '@humanwhocodes/object-schema': 1.2.1 105 | debug: 4.3.4 106 | minimatch: 3.1.2 107 | transitivePeerDependencies: 108 | - supports-color 109 | dev: true 110 | 111 | /@humanwhocodes/module-importer@1.0.1: 112 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 113 | engines: {node: '>=12.22'} 114 | dev: true 115 | 116 | /@humanwhocodes/object-schema@1.2.1: 117 | resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} 118 | dev: true 119 | 120 | /@nodelib/fs.scandir@2.1.5: 121 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 122 | engines: {node: '>= 8'} 123 | dependencies: 124 | '@nodelib/fs.stat': 2.0.5 125 | run-parallel: 1.2.0 126 | dev: true 127 | 128 | /@nodelib/fs.stat@2.0.5: 129 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 130 | engines: {node: '>= 8'} 131 | dev: true 132 | 133 | /@nodelib/fs.walk@1.2.8: 134 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 135 | engines: {node: '>= 8'} 136 | dependencies: 137 | '@nodelib/fs.scandir': 2.1.5 138 | fastq: 1.15.0 139 | dev: true 140 | 141 | /@types/codemirror@5.60.8: 142 | resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==} 143 | dependencies: 144 | '@types/tern': 0.23.4 145 | dev: true 146 | 147 | /@types/estree@1.0.1: 148 | resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} 149 | dev: true 150 | 151 | /@types/json-schema@7.0.12: 152 | resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} 153 | dev: true 154 | 155 | /@types/lodash@4.14.197: 156 | resolution: {integrity: sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==} 157 | dev: false 158 | 159 | /@types/node@16.18.46: 160 | resolution: {integrity: sha512-Mnq3O9Xz52exs3mlxMcQuA7/9VFe/dXcrgAyfjLkABIqxXKOgBRjyazTxUbjsxDa4BP7hhPliyjVTP9RDP14xg==} 161 | dev: true 162 | 163 | /@types/tern@0.23.4: 164 | resolution: {integrity: sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==} 165 | dependencies: 166 | '@types/estree': 1.0.1 167 | dev: true 168 | 169 | /@typescript-eslint/eslint-plugin@5.29.0(@typescript-eslint/parser@5.29.0)(eslint@8.48.0)(typescript@4.7.4): 170 | resolution: {integrity: sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==} 171 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 172 | peerDependencies: 173 | '@typescript-eslint/parser': ^5.0.0 174 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 175 | typescript: '*' 176 | peerDependenciesMeta: 177 | typescript: 178 | optional: true 179 | dependencies: 180 | '@typescript-eslint/parser': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 181 | '@typescript-eslint/scope-manager': 5.29.0 182 | '@typescript-eslint/type-utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 183 | '@typescript-eslint/utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 184 | debug: 4.3.4 185 | eslint: 8.48.0 186 | functional-red-black-tree: 1.0.1 187 | ignore: 5.2.4 188 | regexpp: 3.2.0 189 | semver: 7.5.4 190 | tsutils: 3.21.0(typescript@4.7.4) 191 | typescript: 4.7.4 192 | transitivePeerDependencies: 193 | - supports-color 194 | dev: true 195 | 196 | /@typescript-eslint/parser@5.29.0(eslint@8.48.0)(typescript@4.7.4): 197 | resolution: {integrity: sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==} 198 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 199 | peerDependencies: 200 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 201 | typescript: '*' 202 | peerDependenciesMeta: 203 | typescript: 204 | optional: true 205 | dependencies: 206 | '@typescript-eslint/scope-manager': 5.29.0 207 | '@typescript-eslint/types': 5.29.0 208 | '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) 209 | debug: 4.3.4 210 | eslint: 8.48.0 211 | typescript: 4.7.4 212 | transitivePeerDependencies: 213 | - supports-color 214 | dev: true 215 | 216 | /@typescript-eslint/scope-manager@5.29.0: 217 | resolution: {integrity: sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==} 218 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 219 | dependencies: 220 | '@typescript-eslint/types': 5.29.0 221 | '@typescript-eslint/visitor-keys': 5.29.0 222 | dev: true 223 | 224 | /@typescript-eslint/type-utils@5.29.0(eslint@8.48.0)(typescript@4.7.4): 225 | resolution: {integrity: sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==} 226 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 227 | peerDependencies: 228 | eslint: '*' 229 | typescript: '*' 230 | peerDependenciesMeta: 231 | typescript: 232 | optional: true 233 | dependencies: 234 | '@typescript-eslint/utils': 5.29.0(eslint@8.48.0)(typescript@4.7.4) 235 | debug: 4.3.4 236 | eslint: 8.48.0 237 | tsutils: 3.21.0(typescript@4.7.4) 238 | typescript: 4.7.4 239 | transitivePeerDependencies: 240 | - supports-color 241 | dev: true 242 | 243 | /@typescript-eslint/types@5.29.0: 244 | resolution: {integrity: sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==} 245 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 246 | dev: true 247 | 248 | /@typescript-eslint/typescript-estree@5.29.0(typescript@4.7.4): 249 | resolution: {integrity: sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==} 250 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 251 | peerDependencies: 252 | typescript: '*' 253 | peerDependenciesMeta: 254 | typescript: 255 | optional: true 256 | dependencies: 257 | '@typescript-eslint/types': 5.29.0 258 | '@typescript-eslint/visitor-keys': 5.29.0 259 | debug: 4.3.4 260 | globby: 11.1.0 261 | is-glob: 4.0.3 262 | semver: 7.5.4 263 | tsutils: 3.21.0(typescript@4.7.4) 264 | typescript: 4.7.4 265 | transitivePeerDependencies: 266 | - supports-color 267 | dev: true 268 | 269 | /@typescript-eslint/utils@5.29.0(eslint@8.48.0)(typescript@4.7.4): 270 | resolution: {integrity: sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==} 271 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 272 | peerDependencies: 273 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 274 | dependencies: 275 | '@types/json-schema': 7.0.12 276 | '@typescript-eslint/scope-manager': 5.29.0 277 | '@typescript-eslint/types': 5.29.0 278 | '@typescript-eslint/typescript-estree': 5.29.0(typescript@4.7.4) 279 | eslint: 8.48.0 280 | eslint-scope: 5.1.1 281 | eslint-utils: 3.0.0(eslint@8.48.0) 282 | transitivePeerDependencies: 283 | - supports-color 284 | - typescript 285 | dev: true 286 | 287 | /@typescript-eslint/visitor-keys@5.29.0: 288 | resolution: {integrity: sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==} 289 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 290 | dependencies: 291 | '@typescript-eslint/types': 5.29.0 292 | eslint-visitor-keys: 3.4.3 293 | dev: true 294 | 295 | /acorn-jsx@5.3.2(acorn@8.10.0): 296 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 297 | peerDependencies: 298 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 299 | dependencies: 300 | acorn: 8.10.0 301 | dev: true 302 | 303 | /acorn@8.10.0: 304 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 305 | engines: {node: '>=0.4.0'} 306 | hasBin: true 307 | dev: true 308 | 309 | /ajv@6.12.6: 310 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 311 | dependencies: 312 | fast-deep-equal: 3.1.3 313 | fast-json-stable-stringify: 2.1.0 314 | json-schema-traverse: 0.4.1 315 | uri-js: 4.4.1 316 | dev: true 317 | 318 | /ansi-regex@5.0.1: 319 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 320 | engines: {node: '>=8'} 321 | dev: true 322 | 323 | /ansi-styles@4.3.0: 324 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 325 | engines: {node: '>=8'} 326 | dependencies: 327 | color-convert: 2.0.1 328 | dev: true 329 | 330 | /argparse@2.0.1: 331 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 332 | dev: true 333 | 334 | /array-union@2.1.0: 335 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 336 | engines: {node: '>=8'} 337 | dev: true 338 | 339 | /balanced-match@1.0.2: 340 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 341 | dev: true 342 | 343 | /brace-expansion@1.1.11: 344 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 345 | dependencies: 346 | balanced-match: 1.0.2 347 | concat-map: 0.0.1 348 | dev: true 349 | 350 | /braces@3.0.2: 351 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 352 | engines: {node: '>=8'} 353 | dependencies: 354 | fill-range: 7.0.1 355 | dev: true 356 | 357 | /builtin-modules@3.3.0: 358 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 359 | engines: {node: '>=6'} 360 | dev: true 361 | 362 | /callsites@3.1.0: 363 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 364 | engines: {node: '>=6'} 365 | dev: true 366 | 367 | /chalk@4.1.2: 368 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 369 | engines: {node: '>=10'} 370 | dependencies: 371 | ansi-styles: 4.3.0 372 | supports-color: 7.2.0 373 | dev: true 374 | 375 | /color-convert@2.0.1: 376 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 377 | engines: {node: '>=7.0.0'} 378 | dependencies: 379 | color-name: 1.1.4 380 | dev: true 381 | 382 | /color-name@1.1.4: 383 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 384 | dev: true 385 | 386 | /concat-map@0.0.1: 387 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 388 | dev: true 389 | 390 | /cross-spawn@7.0.3: 391 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 392 | engines: {node: '>= 8'} 393 | dependencies: 394 | path-key: 3.1.1 395 | shebang-command: 2.0.0 396 | which: 2.0.2 397 | dev: true 398 | 399 | /debug@4.3.4: 400 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 401 | engines: {node: '>=6.0'} 402 | peerDependencies: 403 | supports-color: '*' 404 | peerDependenciesMeta: 405 | supports-color: 406 | optional: true 407 | dependencies: 408 | ms: 2.1.2 409 | dev: true 410 | 411 | /deep-is@0.1.4: 412 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 413 | dev: true 414 | 415 | /dir-glob@3.0.1: 416 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 417 | engines: {node: '>=8'} 418 | dependencies: 419 | path-type: 4.0.0 420 | dev: true 421 | 422 | /doctrine@3.0.0: 423 | resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} 424 | engines: {node: '>=6.0.0'} 425 | dependencies: 426 | esutils: 2.0.3 427 | dev: true 428 | 429 | /esbuild-android-64@0.14.47: 430 | resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==} 431 | engines: {node: '>=12'} 432 | cpu: [x64] 433 | os: [android] 434 | requiresBuild: true 435 | dev: true 436 | optional: true 437 | 438 | /esbuild-android-arm64@0.14.47: 439 | resolution: {integrity: sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==} 440 | engines: {node: '>=12'} 441 | cpu: [arm64] 442 | os: [android] 443 | requiresBuild: true 444 | dev: true 445 | optional: true 446 | 447 | /esbuild-darwin-64@0.14.47: 448 | resolution: {integrity: sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==} 449 | engines: {node: '>=12'} 450 | cpu: [x64] 451 | os: [darwin] 452 | requiresBuild: true 453 | dev: true 454 | optional: true 455 | 456 | /esbuild-darwin-arm64@0.14.47: 457 | resolution: {integrity: sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==} 458 | engines: {node: '>=12'} 459 | cpu: [arm64] 460 | os: [darwin] 461 | requiresBuild: true 462 | dev: true 463 | optional: true 464 | 465 | /esbuild-freebsd-64@0.14.47: 466 | resolution: {integrity: sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==} 467 | engines: {node: '>=12'} 468 | cpu: [x64] 469 | os: [freebsd] 470 | requiresBuild: true 471 | dev: true 472 | optional: true 473 | 474 | /esbuild-freebsd-arm64@0.14.47: 475 | resolution: {integrity: sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==} 476 | engines: {node: '>=12'} 477 | cpu: [arm64] 478 | os: [freebsd] 479 | requiresBuild: true 480 | dev: true 481 | optional: true 482 | 483 | /esbuild-linux-32@0.14.47: 484 | resolution: {integrity: sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==} 485 | engines: {node: '>=12'} 486 | cpu: [ia32] 487 | os: [linux] 488 | requiresBuild: true 489 | dev: true 490 | optional: true 491 | 492 | /esbuild-linux-64@0.14.47: 493 | resolution: {integrity: sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==} 494 | engines: {node: '>=12'} 495 | cpu: [x64] 496 | os: [linux] 497 | requiresBuild: true 498 | dev: true 499 | optional: true 500 | 501 | /esbuild-linux-arm64@0.14.47: 502 | resolution: {integrity: sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==} 503 | engines: {node: '>=12'} 504 | cpu: [arm64] 505 | os: [linux] 506 | requiresBuild: true 507 | dev: true 508 | optional: true 509 | 510 | /esbuild-linux-arm@0.14.47: 511 | resolution: {integrity: sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==} 512 | engines: {node: '>=12'} 513 | cpu: [arm] 514 | os: [linux] 515 | requiresBuild: true 516 | dev: true 517 | optional: true 518 | 519 | /esbuild-linux-mips64le@0.14.47: 520 | resolution: {integrity: sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==} 521 | engines: {node: '>=12'} 522 | cpu: [mips64el] 523 | os: [linux] 524 | requiresBuild: true 525 | dev: true 526 | optional: true 527 | 528 | /esbuild-linux-ppc64le@0.14.47: 529 | resolution: {integrity: sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==} 530 | engines: {node: '>=12'} 531 | cpu: [ppc64] 532 | os: [linux] 533 | requiresBuild: true 534 | dev: true 535 | optional: true 536 | 537 | /esbuild-linux-riscv64@0.14.47: 538 | resolution: {integrity: sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==} 539 | engines: {node: '>=12'} 540 | cpu: [riscv64] 541 | os: [linux] 542 | requiresBuild: true 543 | dev: true 544 | optional: true 545 | 546 | /esbuild-linux-s390x@0.14.47: 547 | resolution: {integrity: sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==} 548 | engines: {node: '>=12'} 549 | cpu: [s390x] 550 | os: [linux] 551 | requiresBuild: true 552 | dev: true 553 | optional: true 554 | 555 | /esbuild-netbsd-64@0.14.47: 556 | resolution: {integrity: sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==} 557 | engines: {node: '>=12'} 558 | cpu: [x64] 559 | os: [netbsd] 560 | requiresBuild: true 561 | dev: true 562 | optional: true 563 | 564 | /esbuild-openbsd-64@0.14.47: 565 | resolution: {integrity: sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==} 566 | engines: {node: '>=12'} 567 | cpu: [x64] 568 | os: [openbsd] 569 | requiresBuild: true 570 | dev: true 571 | optional: true 572 | 573 | /esbuild-sunos-64@0.14.47: 574 | resolution: {integrity: sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==} 575 | engines: {node: '>=12'} 576 | cpu: [x64] 577 | os: [sunos] 578 | requiresBuild: true 579 | dev: true 580 | optional: true 581 | 582 | /esbuild-windows-32@0.14.47: 583 | resolution: {integrity: sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==} 584 | engines: {node: '>=12'} 585 | cpu: [ia32] 586 | os: [win32] 587 | requiresBuild: true 588 | dev: true 589 | optional: true 590 | 591 | /esbuild-windows-64@0.14.47: 592 | resolution: {integrity: sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==} 593 | engines: {node: '>=12'} 594 | cpu: [x64] 595 | os: [win32] 596 | requiresBuild: true 597 | dev: true 598 | optional: true 599 | 600 | /esbuild-windows-arm64@0.14.47: 601 | resolution: {integrity: sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==} 602 | engines: {node: '>=12'} 603 | cpu: [arm64] 604 | os: [win32] 605 | requiresBuild: true 606 | dev: true 607 | optional: true 608 | 609 | /esbuild@0.14.47: 610 | resolution: {integrity: sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==} 611 | engines: {node: '>=12'} 612 | hasBin: true 613 | requiresBuild: true 614 | optionalDependencies: 615 | esbuild-android-64: 0.14.47 616 | esbuild-android-arm64: 0.14.47 617 | esbuild-darwin-64: 0.14.47 618 | esbuild-darwin-arm64: 0.14.47 619 | esbuild-freebsd-64: 0.14.47 620 | esbuild-freebsd-arm64: 0.14.47 621 | esbuild-linux-32: 0.14.47 622 | esbuild-linux-64: 0.14.47 623 | esbuild-linux-arm: 0.14.47 624 | esbuild-linux-arm64: 0.14.47 625 | esbuild-linux-mips64le: 0.14.47 626 | esbuild-linux-ppc64le: 0.14.47 627 | esbuild-linux-riscv64: 0.14.47 628 | esbuild-linux-s390x: 0.14.47 629 | esbuild-netbsd-64: 0.14.47 630 | esbuild-openbsd-64: 0.14.47 631 | esbuild-sunos-64: 0.14.47 632 | esbuild-windows-32: 0.14.47 633 | esbuild-windows-64: 0.14.47 634 | esbuild-windows-arm64: 0.14.47 635 | dev: true 636 | 637 | /escape-string-regexp@4.0.0: 638 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 639 | engines: {node: '>=10'} 640 | dev: true 641 | 642 | /eslint-scope@5.1.1: 643 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 644 | engines: {node: '>=8.0.0'} 645 | dependencies: 646 | esrecurse: 4.3.0 647 | estraverse: 4.3.0 648 | dev: true 649 | 650 | /eslint-scope@7.2.2: 651 | resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} 652 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 653 | dependencies: 654 | esrecurse: 4.3.0 655 | estraverse: 5.3.0 656 | dev: true 657 | 658 | /eslint-utils@3.0.0(eslint@8.48.0): 659 | resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} 660 | engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} 661 | peerDependencies: 662 | eslint: '>=5' 663 | dependencies: 664 | eslint: 8.48.0 665 | eslint-visitor-keys: 2.1.0 666 | dev: true 667 | 668 | /eslint-visitor-keys@2.1.0: 669 | resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} 670 | engines: {node: '>=10'} 671 | dev: true 672 | 673 | /eslint-visitor-keys@3.4.3: 674 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 675 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 676 | dev: true 677 | 678 | /eslint@8.48.0: 679 | resolution: {integrity: sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==} 680 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 681 | hasBin: true 682 | dependencies: 683 | '@eslint-community/eslint-utils': 4.4.0(eslint@8.48.0) 684 | '@eslint-community/regexpp': 4.8.0 685 | '@eslint/eslintrc': 2.1.2 686 | '@eslint/js': 8.48.0 687 | '@humanwhocodes/config-array': 0.11.10 688 | '@humanwhocodes/module-importer': 1.0.1 689 | '@nodelib/fs.walk': 1.2.8 690 | ajv: 6.12.6 691 | chalk: 4.1.2 692 | cross-spawn: 7.0.3 693 | debug: 4.3.4 694 | doctrine: 3.0.0 695 | escape-string-regexp: 4.0.0 696 | eslint-scope: 7.2.2 697 | eslint-visitor-keys: 3.4.3 698 | espree: 9.6.1 699 | esquery: 1.5.0 700 | esutils: 2.0.3 701 | fast-deep-equal: 3.1.3 702 | file-entry-cache: 6.0.1 703 | find-up: 5.0.0 704 | glob-parent: 6.0.2 705 | globals: 13.21.0 706 | graphemer: 1.4.0 707 | ignore: 5.2.4 708 | imurmurhash: 0.1.4 709 | is-glob: 4.0.3 710 | is-path-inside: 3.0.3 711 | js-yaml: 4.1.0 712 | json-stable-stringify-without-jsonify: 1.0.1 713 | levn: 0.4.1 714 | lodash.merge: 4.6.2 715 | minimatch: 3.1.2 716 | natural-compare: 1.4.0 717 | optionator: 0.9.3 718 | strip-ansi: 6.0.1 719 | text-table: 0.2.0 720 | transitivePeerDependencies: 721 | - supports-color 722 | dev: true 723 | 724 | /espree@9.6.1: 725 | resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} 726 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 727 | dependencies: 728 | acorn: 8.10.0 729 | acorn-jsx: 5.3.2(acorn@8.10.0) 730 | eslint-visitor-keys: 3.4.3 731 | dev: true 732 | 733 | /esquery@1.5.0: 734 | resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} 735 | engines: {node: '>=0.10'} 736 | dependencies: 737 | estraverse: 5.3.0 738 | dev: true 739 | 740 | /esrecurse@4.3.0: 741 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 742 | engines: {node: '>=4.0'} 743 | dependencies: 744 | estraverse: 5.3.0 745 | dev: true 746 | 747 | /estraverse@4.3.0: 748 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 749 | engines: {node: '>=4.0'} 750 | dev: true 751 | 752 | /estraverse@5.3.0: 753 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 754 | engines: {node: '>=4.0'} 755 | dev: true 756 | 757 | /esutils@2.0.3: 758 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 759 | engines: {node: '>=0.10.0'} 760 | dev: true 761 | 762 | /fast-deep-equal@3.1.3: 763 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 764 | dev: true 765 | 766 | /fast-glob@3.3.1: 767 | resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} 768 | engines: {node: '>=8.6.0'} 769 | dependencies: 770 | '@nodelib/fs.stat': 2.0.5 771 | '@nodelib/fs.walk': 1.2.8 772 | glob-parent: 5.1.2 773 | merge2: 1.4.1 774 | micromatch: 4.0.5 775 | dev: true 776 | 777 | /fast-json-stable-stringify@2.1.0: 778 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 779 | dev: true 780 | 781 | /fast-levenshtein@2.0.6: 782 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 783 | dev: true 784 | 785 | /fastq@1.15.0: 786 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} 787 | dependencies: 788 | reusify: 1.0.4 789 | dev: true 790 | 791 | /file-entry-cache@6.0.1: 792 | resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} 793 | engines: {node: ^10.12.0 || >=12.0.0} 794 | dependencies: 795 | flat-cache: 3.1.0 796 | dev: true 797 | 798 | /fill-range@7.0.1: 799 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 800 | engines: {node: '>=8'} 801 | dependencies: 802 | to-regex-range: 5.0.1 803 | dev: true 804 | 805 | /find-up@5.0.0: 806 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 807 | engines: {node: '>=10'} 808 | dependencies: 809 | locate-path: 6.0.0 810 | path-exists: 4.0.0 811 | dev: true 812 | 813 | /flat-cache@3.1.0: 814 | resolution: {integrity: sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==} 815 | engines: {node: '>=12.0.0'} 816 | dependencies: 817 | flatted: 3.2.7 818 | keyv: 4.5.3 819 | rimraf: 3.0.2 820 | dev: true 821 | 822 | /flatted@3.2.7: 823 | resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} 824 | dev: true 825 | 826 | /fs.realpath@1.0.0: 827 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 828 | dev: true 829 | 830 | /functional-red-black-tree@1.0.1: 831 | resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} 832 | dev: true 833 | 834 | /glob-parent@5.1.2: 835 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 836 | engines: {node: '>= 6'} 837 | dependencies: 838 | is-glob: 4.0.3 839 | dev: true 840 | 841 | /glob-parent@6.0.2: 842 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 843 | engines: {node: '>=10.13.0'} 844 | dependencies: 845 | is-glob: 4.0.3 846 | dev: true 847 | 848 | /glob@7.2.3: 849 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 850 | dependencies: 851 | fs.realpath: 1.0.0 852 | inflight: 1.0.6 853 | inherits: 2.0.4 854 | minimatch: 3.1.2 855 | once: 1.4.0 856 | path-is-absolute: 1.0.1 857 | dev: true 858 | 859 | /globals@13.21.0: 860 | resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} 861 | engines: {node: '>=8'} 862 | dependencies: 863 | type-fest: 0.20.2 864 | dev: true 865 | 866 | /globby@11.1.0: 867 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 868 | engines: {node: '>=10'} 869 | dependencies: 870 | array-union: 2.1.0 871 | dir-glob: 3.0.1 872 | fast-glob: 3.3.1 873 | ignore: 5.2.4 874 | merge2: 1.4.1 875 | slash: 3.0.0 876 | dev: true 877 | 878 | /graphemer@1.4.0: 879 | resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 880 | dev: true 881 | 882 | /has-flag@4.0.0: 883 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 884 | engines: {node: '>=8'} 885 | dev: true 886 | 887 | /ignore@5.2.4: 888 | resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} 889 | engines: {node: '>= 4'} 890 | dev: true 891 | 892 | /import-fresh@3.3.0: 893 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 894 | engines: {node: '>=6'} 895 | dependencies: 896 | parent-module: 1.0.1 897 | resolve-from: 4.0.0 898 | dev: true 899 | 900 | /imurmurhash@0.1.4: 901 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 902 | engines: {node: '>=0.8.19'} 903 | dev: true 904 | 905 | /inflight@1.0.6: 906 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 907 | dependencies: 908 | once: 1.4.0 909 | wrappy: 1.0.2 910 | dev: true 911 | 912 | /inherits@2.0.4: 913 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 914 | dev: true 915 | 916 | /is-extglob@2.1.1: 917 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 918 | engines: {node: '>=0.10.0'} 919 | dev: true 920 | 921 | /is-glob@4.0.3: 922 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 923 | engines: {node: '>=0.10.0'} 924 | dependencies: 925 | is-extglob: 2.1.1 926 | dev: true 927 | 928 | /is-number@7.0.0: 929 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 930 | engines: {node: '>=0.12.0'} 931 | dev: true 932 | 933 | /is-path-inside@3.0.3: 934 | resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} 935 | engines: {node: '>=8'} 936 | dev: true 937 | 938 | /isexe@2.0.0: 939 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 940 | dev: true 941 | 942 | /js-yaml@4.1.0: 943 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 944 | hasBin: true 945 | dependencies: 946 | argparse: 2.0.1 947 | dev: true 948 | 949 | /json-buffer@3.0.1: 950 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 951 | dev: true 952 | 953 | /json-schema-traverse@0.4.1: 954 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 955 | dev: true 956 | 957 | /json-stable-stringify-without-jsonify@1.0.1: 958 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 959 | dev: true 960 | 961 | /keyv@4.5.3: 962 | resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} 963 | dependencies: 964 | json-buffer: 3.0.1 965 | dev: true 966 | 967 | /levn@0.4.1: 968 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 969 | engines: {node: '>= 0.8.0'} 970 | dependencies: 971 | prelude-ls: 1.2.1 972 | type-check: 0.4.0 973 | dev: true 974 | 975 | /locate-path@6.0.0: 976 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 977 | engines: {node: '>=10'} 978 | dependencies: 979 | p-locate: 5.0.0 980 | dev: true 981 | 982 | /lodash.merge@4.6.2: 983 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 984 | dev: true 985 | 986 | /lodash@4.17.21: 987 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 988 | dev: false 989 | 990 | /lru-cache@6.0.0: 991 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 992 | engines: {node: '>=10'} 993 | dependencies: 994 | yallist: 4.0.0 995 | dev: true 996 | 997 | /merge2@1.4.1: 998 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 999 | engines: {node: '>= 8'} 1000 | dev: true 1001 | 1002 | /micromatch@4.0.5: 1003 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 1004 | engines: {node: '>=8.6'} 1005 | dependencies: 1006 | braces: 3.0.2 1007 | picomatch: 2.3.1 1008 | dev: true 1009 | 1010 | /minimatch@3.1.2: 1011 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1012 | dependencies: 1013 | brace-expansion: 1.1.11 1014 | dev: true 1015 | 1016 | /moment@2.29.4: 1017 | resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} 1018 | dev: true 1019 | 1020 | /monkey-around@2.3.0: 1021 | resolution: {integrity: sha512-QWcCUWjqE/MCk9cXlSKZ1Qc486LD439xw/Ak8Nt6l2PuL9+yrc9TJakt7OHDuOqPRYY4nTWBAEFKn32PE/SfXA==} 1022 | dev: false 1023 | 1024 | /ms@2.1.2: 1025 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 1026 | dev: true 1027 | 1028 | /natural-compare@1.4.0: 1029 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1030 | dev: true 1031 | 1032 | /obsidian@1.4.4(@codemirror/state@6.2.1)(@codemirror/view@6.16.0): 1033 | resolution: {integrity: sha512-q2V5GNT/M40uYOENdVw5kovPSoaO6vppiiyBCkIqWgKp4oN654jA/GQ0OaNBA7p5NdfS245QCeRgCFQ42wOZiw==} 1034 | peerDependencies: 1035 | '@codemirror/state': ^6.0.0 1036 | '@codemirror/view': ^6.0.0 1037 | dependencies: 1038 | '@codemirror/state': 6.2.1 1039 | '@codemirror/view': 6.16.0 1040 | '@types/codemirror': 5.60.8 1041 | moment: 2.29.4 1042 | dev: true 1043 | 1044 | /once@1.4.0: 1045 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1046 | dependencies: 1047 | wrappy: 1.0.2 1048 | dev: true 1049 | 1050 | /optionator@0.9.3: 1051 | resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} 1052 | engines: {node: '>= 0.8.0'} 1053 | dependencies: 1054 | '@aashutoshrathi/word-wrap': 1.2.6 1055 | deep-is: 0.1.4 1056 | fast-levenshtein: 2.0.6 1057 | levn: 0.4.1 1058 | prelude-ls: 1.2.1 1059 | type-check: 0.4.0 1060 | dev: true 1061 | 1062 | /p-limit@3.1.0: 1063 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1064 | engines: {node: '>=10'} 1065 | dependencies: 1066 | yocto-queue: 0.1.0 1067 | dev: true 1068 | 1069 | /p-locate@5.0.0: 1070 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1071 | engines: {node: '>=10'} 1072 | dependencies: 1073 | p-limit: 3.1.0 1074 | dev: true 1075 | 1076 | /parent-module@1.0.1: 1077 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1078 | engines: {node: '>=6'} 1079 | dependencies: 1080 | callsites: 3.1.0 1081 | dev: true 1082 | 1083 | /path-exists@4.0.0: 1084 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1085 | engines: {node: '>=8'} 1086 | dev: true 1087 | 1088 | /path-is-absolute@1.0.1: 1089 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1090 | engines: {node: '>=0.10.0'} 1091 | dev: true 1092 | 1093 | /path-key@3.1.1: 1094 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1095 | engines: {node: '>=8'} 1096 | dev: true 1097 | 1098 | /path-type@4.0.0: 1099 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 1100 | engines: {node: '>=8'} 1101 | dev: true 1102 | 1103 | /picomatch@2.3.1: 1104 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1105 | engines: {node: '>=8.6'} 1106 | dev: true 1107 | 1108 | /prelude-ls@1.2.1: 1109 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1110 | engines: {node: '>= 0.8.0'} 1111 | dev: true 1112 | 1113 | /punycode@2.3.0: 1114 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 1115 | engines: {node: '>=6'} 1116 | dev: true 1117 | 1118 | /queue-microtask@1.2.3: 1119 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1120 | dev: true 1121 | 1122 | /regexpp@3.2.0: 1123 | resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} 1124 | engines: {node: '>=8'} 1125 | dev: true 1126 | 1127 | /resolve-from@4.0.0: 1128 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1129 | engines: {node: '>=4'} 1130 | dev: true 1131 | 1132 | /reusify@1.0.4: 1133 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 1134 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1135 | dev: true 1136 | 1137 | /rimraf@3.0.2: 1138 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 1139 | hasBin: true 1140 | dependencies: 1141 | glob: 7.2.3 1142 | dev: true 1143 | 1144 | /run-parallel@1.2.0: 1145 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1146 | dependencies: 1147 | queue-microtask: 1.2.3 1148 | dev: true 1149 | 1150 | /semver@7.5.4: 1151 | resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} 1152 | engines: {node: '>=10'} 1153 | hasBin: true 1154 | dependencies: 1155 | lru-cache: 6.0.0 1156 | dev: true 1157 | 1158 | /shebang-command@2.0.0: 1159 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1160 | engines: {node: '>=8'} 1161 | dependencies: 1162 | shebang-regex: 3.0.0 1163 | dev: true 1164 | 1165 | /shebang-regex@3.0.0: 1166 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1167 | engines: {node: '>=8'} 1168 | dev: true 1169 | 1170 | /slash@3.0.0: 1171 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 1172 | engines: {node: '>=8'} 1173 | dev: true 1174 | 1175 | /strip-ansi@6.0.1: 1176 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1177 | engines: {node: '>=8'} 1178 | dependencies: 1179 | ansi-regex: 5.0.1 1180 | dev: true 1181 | 1182 | /strip-json-comments@3.1.1: 1183 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1184 | engines: {node: '>=8'} 1185 | dev: true 1186 | 1187 | /style-mod@4.1.0: 1188 | resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==} 1189 | dev: true 1190 | 1191 | /supports-color@7.2.0: 1192 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1193 | engines: {node: '>=8'} 1194 | dependencies: 1195 | has-flag: 4.0.0 1196 | dev: true 1197 | 1198 | /text-table@0.2.0: 1199 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 1200 | dev: true 1201 | 1202 | /to-regex-range@5.0.1: 1203 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1204 | engines: {node: '>=8.0'} 1205 | dependencies: 1206 | is-number: 7.0.0 1207 | dev: true 1208 | 1209 | /tslib@1.14.1: 1210 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} 1211 | dev: true 1212 | 1213 | /tslib@2.4.0: 1214 | resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} 1215 | dev: true 1216 | 1217 | /tsutils@3.21.0(typescript@4.7.4): 1218 | resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} 1219 | engines: {node: '>= 6'} 1220 | peerDependencies: 1221 | typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' 1222 | dependencies: 1223 | tslib: 1.14.1 1224 | typescript: 4.7.4 1225 | dev: true 1226 | 1227 | /type-check@0.4.0: 1228 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1229 | engines: {node: '>= 0.8.0'} 1230 | dependencies: 1231 | prelude-ls: 1.2.1 1232 | dev: true 1233 | 1234 | /type-fest@0.20.2: 1235 | resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} 1236 | engines: {node: '>=10'} 1237 | dev: true 1238 | 1239 | /typescript@4.7.4: 1240 | resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} 1241 | engines: {node: '>=4.2.0'} 1242 | hasBin: true 1243 | dev: true 1244 | 1245 | /uri-js@4.4.1: 1246 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1247 | dependencies: 1248 | punycode: 2.3.0 1249 | dev: true 1250 | 1251 | /w3c-keyname@2.2.8: 1252 | resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} 1253 | dev: true 1254 | 1255 | /which@2.0.2: 1256 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1257 | engines: {node: '>= 8'} 1258 | hasBin: true 1259 | dependencies: 1260 | isexe: 2.0.0 1261 | dev: true 1262 | 1263 | /wrappy@1.0.2: 1264 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1265 | dev: true 1266 | 1267 | /yallist@4.0.0: 1268 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 1269 | dev: true 1270 | 1271 | /yocto-queue@0.1.0: 1272 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1273 | engines: {node: '>=10'} 1274 | dev: true 1275 | -------------------------------------------------------------------------------- /src/AugmentedCanvasPlugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Canvas, 3 | CanvasView, 4 | ItemView, 5 | Menu, 6 | MenuItem, 7 | Notice, 8 | Plugin, 9 | TFolder, 10 | setIcon, 11 | setTooltip, 12 | } from "obsidian"; 13 | import { around } from "monkey-around"; 14 | import { 15 | addAskAIButton, 16 | addRegenerateResponse, 17 | handleCallGPT_Question, 18 | } from "./actions/canvasNodeMenuActions/advancedCanvas"; 19 | import { 20 | AugmentedCanvasSettings, 21 | DEFAULT_SETTINGS, 22 | SystemPrompt, 23 | } from "./settings/AugmentedCanvasSettings"; 24 | import SettingsTab from "./settings/SettingsTab"; 25 | import { CustomQuestionModal } from "./modals/CustomQuestionModal"; 26 | import { CanvasNode } from "./obsidian/canvas-internal"; 27 | import { handlePatchNoteMenu } from "./actions/menuPatches/noteMenuPatch"; 28 | import { createCanvasGroup, getActiveCanvas } from "./utils"; 29 | import SystemPromptsModal from "./modals/SystemPromptsModal"; 30 | 31 | import { createFlashcards } from "./actions/canvasNodeContextMenuActions/flashcards"; 32 | import { getFilesContent } from "./obsidian/fileUtil"; 33 | import { getResponse } from "./utils/chatgpt"; 34 | import { parseCsv } from "./utils/csvUtils"; 35 | import { handleAddRelevantQuestions } from "./actions/commands/relevantQuestions"; 36 | import { handleGenerateImage } from "./actions/canvasNodeContextMenuActions/generateImage"; 37 | import { initLogDebug } from "./logDebug"; 38 | import FolderSuggestModal from "./modals/FolderSuggestModal"; 39 | import { calcHeight, createNode } from "./obsidian/canvas-patches"; 40 | import { insertSystemPrompt } from "./actions/commands/insertSystemPrompt"; 41 | import { runPromptFolder } from "./actions/commands/runPromptFolder"; 42 | import { InputModal } from "./modals/InputModal"; 43 | import { runYoutubeCaptions } from "./actions/commands/youtubeCaptions"; 44 | import { insertWebsiteContent } from "./actions/commands/websiteContent"; 45 | 46 | // @ts-expect-error 47 | import promptsCsvText from "./data/prompts.csv.txt"; 48 | 49 | export default class AugmentedCanvasPlugin extends Plugin { 50 | triggerByPlugin: boolean = false; 51 | patchSucceed: boolean = false; 52 | 53 | settings: AugmentedCanvasSettings; 54 | 55 | async onload() { 56 | await this.loadSettings(); 57 | this.addSettingTab(new SettingsTab(this.app, this)); 58 | 59 | // this.registerCommands(); 60 | // this.registerCanvasEvents(); 61 | // this.registerCustomIcons(); 62 | 63 | // this.patchCanvas(); 64 | this.app.workspace.onLayoutReady(() => { 65 | initLogDebug(this.settings); 66 | 67 | this.patchCanvasMenu(); 68 | this.addCommands(); 69 | this.patchNoteContextMenu(); 70 | 71 | if (this.settings.systemPrompts.length === 0) { 72 | this.fetchSystemPrompts(); 73 | } 74 | }); 75 | // this.patchCanvasInteraction(); 76 | // this.patchCanvasNode(); 77 | 78 | // const generator = noteGenerator(this.app, this.settings, this.logDebug) 79 | // const generator = noteGenerator(this.app); 80 | 81 | // this.addSettingTab(new SettingsTab(this.app, this)) 82 | 83 | // this.addCommand({ 84 | // id: "next-note", 85 | // name: "Create next note", 86 | // callback: () => { 87 | // generator.nextNote(); 88 | // }, 89 | // hotkeys: [ 90 | // { 91 | // modifiers: ["Alt", "Shift"], 92 | // key: "N", 93 | // }, 94 | // ], 95 | // }); 96 | 97 | // this.addCommand({ 98 | // id: "generate-note", 99 | // name: "Generate AI note", 100 | // callback: () => { 101 | // generator.generateNote(); 102 | // }, 103 | // hotkeys: [ 104 | // { 105 | // modifiers: ["Alt", "Shift"], 106 | // key: "G", 107 | // }, 108 | // ], 109 | // }); 110 | } 111 | 112 | onunload() { 113 | // refreshAllCanvasView(this.app); 114 | } 115 | 116 | async loadSettings() { 117 | this.settings = Object.assign( 118 | {}, 119 | DEFAULT_SETTINGS, 120 | await this.loadData() 121 | ); 122 | } 123 | 124 | patchCanvasMenu() { 125 | const app = this.app; 126 | const settings = this.settings; 127 | 128 | const patchMenu = () => { 129 | const canvasView = this.app.workspace 130 | .getLeavesOfType("canvas") 131 | .first()?.view; 132 | if (!canvasView) return false; 133 | 134 | // console.log("canvasView", canvasView); 135 | // TODO: check if this is working (not working in my vault, but works in the sample vault (no .canvas ...)) 136 | const menu = (canvasView as CanvasView)?.canvas?.menu; 137 | if (!menu) return false; 138 | 139 | const selection = menu.selection; 140 | if (!selection) return false; 141 | 142 | const menuUninstaller = around(menu.constructor.prototype, { 143 | render: (next: any) => 144 | function (...args: any) { 145 | const result = next.call(this, ...args); 146 | 147 | // * If multi selection 148 | const maybeCanvasView = 149 | app.workspace.getActiveViewOfType( 150 | ItemView 151 | ) as CanvasView | null; 152 | if ( 153 | !maybeCanvasView || 154 | maybeCanvasView.canvas?.selection?.size !== 1 155 | ) 156 | return result; 157 | 158 | // // * If group 159 | // if (node.unknownData.type === "group") return result; 160 | 161 | if (this.menuEl.querySelector(".gpt-menu-item")) 162 | return result; 163 | 164 | // * If Edge 165 | const selectedNode = Array.from( 166 | maybeCanvasView.canvas?.selection 167 | )[0]; 168 | if ( 169 | // @ts-expect-error 170 | selectedNode.from 171 | ) { 172 | if (!selectedNode.unknownData.isGenerated) return; 173 | addRegenerateResponse(app, settings, this.menuEl); 174 | } else { 175 | // * Handles "Call GPT" button 176 | 177 | addAskAIButton(app, settings, this.menuEl); 178 | 179 | // const node = ( 180 | // Array.from(this.canvas.selection)?.first() 181 | // ); 182 | 183 | // if (!node?.unknownData.questions?.length) return; 184 | 185 | // * Handles "Ask Question" button 186 | // TODO: refactor (as above) 187 | 188 | const buttonEl_AskQuestion = createEl( 189 | "button", 190 | "clickable-icon gpt-menu-item" 191 | ); 192 | setTooltip( 193 | buttonEl_AskQuestion, 194 | "Ask question with AI", 195 | { 196 | placement: "top", 197 | } 198 | ); 199 | setIcon(buttonEl_AskQuestion, "lucide-help-circle"); 200 | this.menuEl.appendChild(buttonEl_AskQuestion); 201 | buttonEl_AskQuestion.addEventListener( 202 | "click", 203 | () => { 204 | let modal = new CustomQuestionModal( 205 | app, 206 | (question2: string) => { 207 | handleCallGPT_Question( 208 | app, 209 | settings, 210 | ( 211 | Array.from( 212 | this.canvas.selection 213 | )?.first()! 214 | ), 215 | question2 216 | ); 217 | // Handle the input 218 | } 219 | ); 220 | modal.open(); 221 | } 222 | ); 223 | 224 | // * Handles "AI Questions" button 225 | 226 | const buttonEl_AIQuestions = createEl( 227 | "button", 228 | "clickable-icon gpt-menu-item" 229 | ); 230 | setTooltip( 231 | buttonEl_AIQuestions, 232 | "AI generated questions", 233 | { 234 | placement: "top", 235 | } 236 | ); 237 | setIcon( 238 | buttonEl_AIQuestions, 239 | "lucide-file-question" 240 | ); 241 | this.menuEl.appendChild(buttonEl_AIQuestions); 242 | buttonEl_AIQuestions.addEventListener("click", () => 243 | handlePatchNoteMenu( 244 | buttonEl_AIQuestions, 245 | this.menuEl, 246 | { 247 | app, 248 | settings, 249 | canvas: this.canvas, 250 | } 251 | ) 252 | ); 253 | } 254 | return result; 255 | }, 256 | }); 257 | 258 | this.register(menuUninstaller); 259 | this.app.workspace.trigger("collapse-node:patched-canvas"); 260 | 261 | return true; 262 | }; 263 | 264 | this.app.workspace.onLayoutReady(() => { 265 | if (!patchMenu()) { 266 | const evt = this.app.workspace.on("layout-change", () => { 267 | patchMenu() && this.app.workspace.offref(evt); 268 | }); 269 | this.registerEvent(evt); 270 | } 271 | }); 272 | } 273 | 274 | async fetchSystemPrompts() { 275 | // const response = await fetch( 276 | // "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv" 277 | // ); 278 | // const text = await response.text(); 279 | const parsedCsv = parseCsv(promptsCsvText); 280 | // console.log({ parsedCsv }); 281 | 282 | const systemPrompts: SystemPrompt[] = parsedCsv 283 | .slice(1) 284 | .map((value: string[], index: number) => ({ 285 | id: index, 286 | act: value[0], 287 | prompt: value[1], 288 | })); 289 | // console.log({ systemPrompts }); 290 | 291 | this.settings.systemPrompts = systemPrompts; 292 | 293 | this.saveSettings(); 294 | } 295 | 296 | patchNoteContextMenu() { 297 | const settings = this.settings; 298 | // * no event name to add to Canvas context menu ("canvas-menu" does not exist) 299 | this.registerEvent( 300 | this.app.workspace.on("canvas:node-menu", (menu) => { 301 | menu.addSeparator(); 302 | menu.addItem((item) => { 303 | item.setTitle("Create flashcards") 304 | .setIcon("lucide-wallet-cards") 305 | .onClick(() => { 306 | createFlashcards(this.app, settings); 307 | }); 308 | }); 309 | menu.addItem((item) => { 310 | item.setTitle("Generate image") 311 | .setIcon("lucide-image") 312 | .onClick(() => { 313 | handleGenerateImage(this.app, settings); 314 | }); 315 | }); 316 | }) 317 | ); 318 | } 319 | 320 | addCommands() { 321 | const app = this.app; 322 | 323 | // * Website to MD 324 | // this.addCommand({ 325 | // id: "insert-website-content", 326 | // name: "Insert the content of a website as markdown", 327 | // checkCallback: (checking: boolean) => { 328 | // if (checking) { 329 | // // console.log({ checkCallback: checking }); 330 | // if (!getActiveCanvas(app)) return false; 331 | 332 | // return true; 333 | // } 334 | 335 | // new InputModal( 336 | // app, 337 | // { 338 | // label: "Enter a website url", 339 | // buttonLabel: "Get website content", 340 | // }, 341 | // (videoUrl: string) => { 342 | // new Notice(`Scraping website content`); 343 | 344 | // insertWebsiteContent(app, this.settings, videoUrl); 345 | // } 346 | // ).open(); 347 | // }, 348 | // // callback: () => {}, 349 | // }); 350 | 351 | // * Youtube captions 352 | // this.addCommand({ 353 | // id: "insert-youtube-caption", 354 | // name: "Insert captions of a Youtube video", 355 | // checkCallback: (checking: boolean) => { 356 | // if (checking) { 357 | // // console.log({ checkCallback: checking }); 358 | // if (!getActiveCanvas(app)) return false; 359 | 360 | // return true; 361 | // } 362 | 363 | // new InputModal( 364 | // app, 365 | // { 366 | // label: "Enter a youtube url", 367 | // buttonLabel: "Scrape captions", 368 | // }, 369 | // (videoUrl: string) => { 370 | // new Notice(`Scraping captions of youtube video`); 371 | 372 | // runYoutubeCaptions(app, this.settings, videoUrl); 373 | // } 374 | // ).open(); 375 | // }, 376 | // // callback: () => {}, 377 | // }); 378 | 379 | this.addCommand({ 380 | id: "run-prompt-folder", 381 | name: "Run a system prompt on a folder", 382 | checkCallback: (checking: boolean) => { 383 | if (checking) { 384 | // console.log({ checkCallback: checking }); 385 | if (!getActiveCanvas(app)) return false; 386 | 387 | return true; 388 | } 389 | 390 | new SystemPromptsModal( 391 | app, 392 | this.settings, 393 | (systemPrompt: SystemPrompt) => { 394 | new Notice( 395 | `Selected system prompt ${systemPrompt.act}` 396 | ); 397 | 398 | new FolderSuggestModal(app, (folder: TFolder) => { 399 | // new Notice(`Selected folder ${folder.path}`); 400 | runPromptFolder( 401 | app, 402 | this.settings, 403 | systemPrompt, 404 | folder 405 | ); 406 | }).open(); 407 | } 408 | ).open(); 409 | }, 410 | // callback: () => {}, 411 | }); 412 | 413 | this.addCommand({ 414 | id: "insert-system-prompt", 415 | name: "Insert system prompt", 416 | checkCallback: (checking: boolean) => { 417 | if (checking) { 418 | // console.log({ checkCallback: checking }); 419 | if (!getActiveCanvas(app)) return false; 420 | 421 | return true; 422 | } 423 | 424 | new SystemPromptsModal( 425 | app, 426 | this.settings, 427 | (systemPrompt: SystemPrompt) => 428 | insertSystemPrompt(app, systemPrompt) 429 | ).open(); 430 | }, 431 | // callback: () => {}, 432 | }); 433 | 434 | this.addCommand({ 435 | id: "insert-relevant-questions", 436 | name: "Insert relevant questions", 437 | checkCallback: (checking: boolean) => { 438 | if (checking) { 439 | // console.log({ checkCallback: checking }); 440 | if (!getActiveCanvas(app)) return false; 441 | return true; 442 | } 443 | 444 | // new SystemPromptsModal(this.app, this.settings).open(); 445 | handleAddRelevantQuestions(app, this.settings); 446 | }, 447 | // callback: async () => {}, 448 | }); 449 | } 450 | 451 | async saveSettings() { 452 | await this.saveData(this.settings); 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /src/Modals/CustomQuestionModal.ts: -------------------------------------------------------------------------------- 1 | import { Modal, App } from "obsidian"; 2 | 3 | export class CustomQuestionModal extends Modal { 4 | onSubmit: (input: string) => void; 5 | 6 | constructor(app: App, onSubmit: (input: string) => void) { 7 | super(app); 8 | this.onSubmit = onSubmit; 9 | } 10 | 11 | onOpen() { 12 | let { contentEl } = this; 13 | contentEl.className = "augmented-canvas-modal-container"; 14 | 15 | let textareaEl = contentEl.createEl("textarea"); 16 | textareaEl.className = "augmented-canvas-modal-textarea"; 17 | textareaEl.placeholder = "Write your question here"; 18 | 19 | // Add keydown event listener to the textarea 20 | textareaEl.addEventListener("keydown", (event) => { 21 | // Check if Ctrl + Enter is pressed 22 | if (event.ctrlKey && event.key === "Enter") { 23 | // Prevent default action to avoid any unwanted behavior 24 | event.preventDefault(); 25 | // Call the onSubmit function and close the modal 26 | this.onSubmit(textareaEl.value); 27 | this.close(); 28 | } 29 | }); 30 | 31 | // Create and append a submit button 32 | let submitBtn = contentEl.createEl("button", { text: "Ask AI" }); 33 | submitBtn.onClickEvent(() => { 34 | this.onSubmit(textareaEl.value); 35 | this.close(); 36 | }); 37 | } 38 | 39 | onClose() { 40 | let { contentEl } = this; 41 | contentEl.empty(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Modals/FolderSuggestModal.ts: -------------------------------------------------------------------------------- 1 | import { App, FuzzySuggestModal, TFile, TFolder } from "obsidian"; 2 | 3 | export default class FolderSuggestModal extends FuzzySuggestModal { 4 | onChoose: (systemPrompt: TFolder) => void; 5 | 6 | constructor(app: App, onChoose: (folder: TFolder) => void) { 7 | super(app); 8 | this.onChoose = onChoose; 9 | } 10 | 11 | getItems(): TFolder[] { 12 | // Get all markdown files and then map to their parent folders, removing duplicates. 13 | return this.app.vault 14 | .getAllLoadedFiles() 15 | .filter((file) => file instanceof TFolder) as TFolder[]; 16 | } 17 | 18 | getItemText(folder: TFolder): string { 19 | // Return a string for the display of each item in the list. 20 | return folder.path; 21 | } 22 | 23 | onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent): void { 24 | this.onChoose(folder); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Modals/InputModal.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, Modal, App, Notice, Setting, Command } from "obsidian"; 2 | 3 | export class InputModal extends Modal { 4 | label: string; 5 | buttonLabel: string; 6 | onSubmit: (value: string) => void; 7 | inputEl: HTMLInputElement; 8 | 9 | constructor( 10 | app: App, 11 | { label, buttonLabel }: { label: string; buttonLabel: string }, 12 | onSubmit: (value: string) => void 13 | ) { 14 | super(app); 15 | this.label = label; 16 | this.buttonLabel = buttonLabel; 17 | this.onSubmit = onSubmit; 18 | } 19 | 20 | onOpen() { 21 | let { contentEl } = this; 22 | contentEl.className = "augmented-canvas-modal-container"; 23 | 24 | let inputEl = contentEl.createEl("input"); 25 | inputEl.className = "augmented-canvas-modal-input"; 26 | inputEl.placeholder = this.label; 27 | 28 | // Add keydown event listener to the textarea 29 | inputEl.addEventListener("keydown", (event) => { 30 | // Check if Ctrl + Enter is pressed 31 | if (event.key === "Enter") { 32 | // Prevent default action to avoid any unwanted behavior 33 | event.preventDefault(); 34 | // Call the onSubmit function and close the modal 35 | this.onSubmit(inputEl.value); 36 | this.close(); 37 | } 38 | }); 39 | 40 | // Create and append a submit button 41 | let submitBtn = contentEl.createEl("button", { 42 | text: this.buttonLabel, 43 | }); 44 | submitBtn.onClickEvent(() => { 45 | this.onSubmit(inputEl.value); 46 | this.close(); 47 | }); 48 | } 49 | 50 | onClose() { 51 | const { contentEl } = this; 52 | contentEl.empty(); 53 | } 54 | 55 | submit() { 56 | const value = this.inputEl.value; 57 | this.onSubmit(value); 58 | this.close(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Modals/SystemPromptsModal.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Notice, SuggestModal, App } from "obsidian"; 2 | import { getActiveCanvas } from "../utils"; 3 | import { 4 | AugmentedCanvasSettings, 5 | SystemPrompt, 6 | } from "../settings/AugmentedCanvasSettings"; 7 | import { calcHeight, createNode } from "../obsidian/canvas-patches"; 8 | import Fuse, { FuseResult } from "fuse.js"; 9 | 10 | /** 11 | * A serchable modal that allows the user to select a checkbox status symbol 12 | */ 13 | export default class QuickActionModal extends SuggestModal { 14 | settings: AugmentedCanvasSettings; 15 | fuse: Fuse; 16 | onChoose: (systemPrompt: SystemPrompt) => void; 17 | 18 | /** 19 | * 20 | * @param app Obsidian instance 21 | * @param plugin plugin instance 22 | * @param editor editor instance 23 | */ 24 | constructor( 25 | app: App, 26 | settings: AugmentedCanvasSettings, 27 | onChoose: (systemPrompt: SystemPrompt) => void 28 | ) { 29 | super(app); 30 | this.settings = settings; 31 | this.onChoose = onChoose; 32 | 33 | const fuse = new Fuse( 34 | [...this.settings.userSystemPrompts, ...this.settings.systemPrompts] 35 | .filter((systemPrompt: SystemPrompt) => systemPrompt.act) 36 | .sort((a, b) => a.act.localeCompare(b.act)), 37 | { 38 | keys: ["act", "prompt"], 39 | } 40 | ); 41 | this.fuse = fuse; 42 | } 43 | 44 | /** 45 | * filters the checkbox options; the results are used as suggestions 46 | * @param query the search string 47 | * @returns collection of options 48 | */ 49 | getSuggestions(query: string): SystemPrompt[] { 50 | if (query === "") return this.settings.systemPrompts; 51 | 52 | return this.fuse 53 | .search(query) 54 | .map((result: FuseResult) => result.item); 55 | } 56 | 57 | /** 58 | * renders each suggestion 59 | * @param option the checkbox option to display 60 | * @param el the suggestion HTML element 61 | */ 62 | renderSuggestion(systemPrompt: SystemPrompt, el: HTMLElement) { 63 | el.setCssStyles({ 64 | display: "flex", 65 | flexDirection: "row", 66 | alignItems: "center", 67 | textAlign: "center", 68 | }); 69 | 70 | const input = el.createEl("span", { 71 | text: systemPrompt.act, 72 | }); 73 | } 74 | 75 | /** 76 | * Handler for when the user chooses an option 77 | * @param option the option selected by the user 78 | * @param evt the triggering mouse or keyboard event 79 | */ 80 | onChooseSuggestion( 81 | systemPrompt: SystemPrompt, 82 | evt: MouseEvent | KeyboardEvent 83 | ) { 84 | this.onChoose(systemPrompt); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/actions/canvasContextMenuActions/flashcards.ts: -------------------------------------------------------------------------------- 1 | import { CanvasView } from "../../obsidian/canvas-patches"; 2 | import { CanvasNode } from "../../obsidian/canvas-internal"; 3 | import { App, Notice } from "obsidian"; 4 | import { getActiveCanvas } from "../../utils"; 5 | import { readNodeContent } from "../../obsidian/fileUtil"; 6 | import { 7 | AugmentedCanvasSettings, 8 | DEFAULT_SETTINGS, 9 | } from "../../settings/AugmentedCanvasSettings"; 10 | import { getResponse } from "../../utils/chatgpt"; 11 | import { getTokenLimit } from "../canvasNodeMenuActions/noteGenerator"; 12 | 13 | const FLASHCARDS_SYSTEM_PROMPT = ` 14 | You must respond in this JSON format: { 15 | "filename": The filename, 16 | "flashcards": { 17 | "front": string, 18 | "back": string 19 | }[] 20 | } 21 | 22 | You must respond in the language the user used, default to english. 23 | `.trim(); 24 | 25 | export const createFlashcards = async ( 26 | app: App, 27 | settings: AugmentedCanvasSettings 28 | ) => { 29 | const canvas = getActiveCanvas(app); 30 | if (!canvas) return; 31 | 32 | new Notice("Flashcard file being created..."); 33 | 34 | const node = Array.from(canvas.selection)?.first()!; 35 | 36 | const nodeText = (await readNodeContent(node))?.trim() || ""; 37 | 38 | // TODO : respect token limit 39 | // const encoding = encodingForModel( 40 | // (settings.apiModel || DEFAULT_SETTINGS.apiModel) as TiktokenModel 41 | // ); 42 | 43 | // const inputLimit = getTokenLimit(settings); 44 | 45 | // let nodeTokens = encoding.encode(nodeText); 46 | 47 | // const keepTokens = nodeTokens.slice(0, inputLimit - tokenCount - 1); 48 | // const truncateTextTo = encoding.decode(keepTokens).length; 49 | // // console.log( 50 | // // `Truncating node text from ${nodeText.length} to ${truncateTextTo} characters` 51 | // // ); 52 | // nodeText = nodeText.slice(0, truncateTextTo); 53 | 54 | const gptResponse = await getResponse( 55 | settings.apiKey, 56 | [ 57 | { 58 | role: "system", 59 | content: `${FLASHCARDS_SYSTEM_PROMPT} 60 | 61 | ${settings.flashcardsSystemPrompt}`, 62 | }, 63 | { 64 | role: "user", 65 | content: nodeText, 66 | }, 67 | ], 68 | { 69 | model: settings.apiModel, 70 | max_tokens: settings.maxResponseTokens || undefined, 71 | temperature: settings.temperature, 72 | isJSON: true, 73 | } 74 | ); 75 | // console.log({ gptResponse }); 76 | 77 | const content = ` 78 | ${gptResponse.flashcards 79 | .map( 80 | (flashcard: { front: string; back: string }) => 81 | `${flashcard.front}::${flashcard.back}` 82 | // `#Q 83 | // ${flashcard.front}::${flashcard.back}` 84 | ) 85 | .join("\n\n")} 86 | `.trim(); 87 | 88 | // TODO : replace with settings value 89 | const FLASHCARDS_PATH = "Home/Flashcards"; 90 | try { 91 | await app.vault.createFolder( 92 | `${FLASHCARDS_PATH}/${gptResponse.filename}` 93 | ); 94 | } catch {} 95 | await app.vault.create( 96 | `${FLASHCARDS_PATH}/${gptResponse.filename}/${gptResponse.filename}.md`, 97 | content 98 | ); 99 | 100 | new Notice(`Flashcard file "${gptResponse.filename}" created successfully`); 101 | 102 | // await app.workspace.openLinkText( 103 | // `Flashcards/${gptResponse.filename}.md`, 104 | // "" 105 | // ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/actions/canvasNodeContextMenuActions/flashcards.ts: -------------------------------------------------------------------------------- 1 | import { CanvasView } from "../../obsidian/canvas-patches"; 2 | import { CanvasNode } from "../../obsidian/canvas-internal"; 3 | import { App, Notice } from "obsidian"; 4 | import { getActiveCanvas } from "../../utils"; 5 | import { readNodeContent } from "../../obsidian/fileUtil"; 6 | import { 7 | AugmentedCanvasSettings, 8 | DEFAULT_SETTINGS, 9 | } from "../../settings/AugmentedCanvasSettings"; 10 | import { getResponse } from "../../utils/chatgpt"; 11 | import { getTokenLimit } from "../canvasNodeMenuActions/noteGenerator"; 12 | 13 | const FLASHCARDS_SYSTEM_PROMPT = ` 14 | You must respond in this JSON format: { 15 | "filename": The filename, 16 | "flashcards": { 17 | "front": string, 18 | "back": string 19 | }[] 20 | } 21 | 22 | You must respond in the language the user used, default to english. 23 | `.trim(); 24 | 25 | export const createFlashcards = async ( 26 | app: App, 27 | settings: AugmentedCanvasSettings 28 | ) => { 29 | const canvas = getActiveCanvas(app); 30 | if (!canvas) return; 31 | 32 | new Notice("Flashcard file being created..."); 33 | 34 | const node = Array.from(canvas.selection)?.first()!; 35 | 36 | const nodeText = (await readNodeContent(node))?.trim() || ""; 37 | 38 | // TODO : respect token limit 39 | // const encoding = encodingForModel( 40 | // (settings.apiModel || DEFAULT_SETTINGS.apiModel) as TiktokenModel 41 | // ); 42 | 43 | // const inputLimit = getTokenLimit(settings); 44 | 45 | // let nodeTokens = encoding.encode(nodeText); 46 | 47 | // const keepTokens = nodeTokens.slice(0, inputLimit - tokenCount - 1); 48 | // const truncateTextTo = encoding.decode(keepTokens).length; 49 | // // console.log( 50 | // // `Truncating node text from ${nodeText.length} to ${truncateTextTo} characters` 51 | // // ); 52 | // nodeText = nodeText.slice(0, truncateTextTo); 53 | 54 | const gptResponse = await getResponse( 55 | settings.apiKey, 56 | [ 57 | { 58 | role: "system", 59 | content: `${FLASHCARDS_SYSTEM_PROMPT} 60 | 61 | ${settings.flashcardsSystemPrompt}`, 62 | }, 63 | { 64 | role: "user", 65 | content: nodeText, 66 | }, 67 | ], 68 | { 69 | model: settings.apiModel, 70 | max_tokens: settings.maxResponseTokens || undefined, 71 | temperature: settings.temperature, 72 | isJSON: true, 73 | } 74 | ); 75 | // console.log({ gptResponse }); 76 | 77 | const content = ` 78 | ${gptResponse.flashcards 79 | .map( 80 | (flashcard: { front: string; back: string }) => 81 | `${flashcard.front}::${flashcard.back}` 82 | // `#Q 83 | // ${flashcard.front}::${flashcard.back}` 84 | ) 85 | .join("\n\n")} 86 | `.trim(); 87 | 88 | // TODO : replace with settings value 89 | const FLASHCARDS_PATH = "Home/Flashcards"; 90 | try { 91 | await app.vault.createFolder( 92 | `${FLASHCARDS_PATH}/${gptResponse.filename}` 93 | ); 94 | } catch {} 95 | await app.vault.create( 96 | `${FLASHCARDS_PATH}/${gptResponse.filename}/${gptResponse.filename}.md`, 97 | content 98 | ); 99 | 100 | new Notice(`Flashcard file "${gptResponse.filename}" created successfully`); 101 | 102 | // await app.workspace.openLinkText( 103 | // `Flashcards/${gptResponse.filename}.md`, 104 | // "" 105 | // ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/actions/canvasNodeContextMenuActions/generateImage.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice, TFile } from "obsidian"; 2 | import { AugmentedCanvasSettings } from "../../settings/AugmentedCanvasSettings"; 3 | import { createImage } from "../../utils/chatgpt"; 4 | import { 5 | getActiveCanvas, 6 | getActiveCanvasNodes, 7 | getCanvasActiveNoteText, 8 | getImageSaveFolderPath, 9 | } from "../../utils"; 10 | import { saveBase64Image } from "../../obsidian/imageUtils"; 11 | import { createNode } from "../../obsidian/canvas-patches"; 12 | import { generateFileName, updateNodeAndSave } from "../../obsidian/fileUtil"; 13 | 14 | export const handleGenerateImage = async ( 15 | app: App, 16 | settings: AugmentedCanvasSettings 17 | ) => { 18 | new Notice(`Generating image using ${settings.imageModel}...`); 19 | 20 | const canvas = getActiveCanvas(app); 21 | if (!canvas) return; 22 | 23 | const activeCanvasNodes = getActiveCanvasNodes(app); 24 | if (!activeCanvasNodes || activeCanvasNodes.length !== 1) return; 25 | 26 | const parentNode = activeCanvasNodes[0]; 27 | 28 | const nodeText = await getCanvasActiveNoteText(app); 29 | if (!nodeText) return; 30 | 31 | const IMAGE_WIDTH = parentNode.width; 32 | const IMAGE_HEIGHT = IMAGE_WIDTH * (1024 / 1792) + 20; 33 | 34 | const node = createNode( 35 | canvas, 36 | { 37 | text: `\`Calling AI (${settings.imageModel})...\``, 38 | size: { 39 | width: IMAGE_WIDTH, 40 | height: IMAGE_HEIGHT, 41 | }, 42 | }, 43 | parentNode 44 | ); 45 | 46 | const b64Image = await createImage(settings.apiKey, nodeText, { 47 | model: settings.imageModel, 48 | }); 49 | 50 | const imageFileName = generateFileName("AI-Image"); 51 | const imageFolder = await getImageSaveFolderPath(app, settings); 52 | // console.log({ imageFolder }); 53 | await saveBase64Image(app, `${imageFolder}/${imageFileName}.png`, b64Image); 54 | new Notice(`Generating image "${imageFileName}" done successfully.`); 55 | 56 | updateNodeAndSave(canvas, node, { 57 | text: `![[${imageFolder}/${imageFileName}.png]]`, 58 | }); 59 | 60 | // TODO : For now Obsidian API to .createFileNode is bugged 61 | // canvas.removeNode(node); 62 | 63 | // await sleep(100); 64 | 65 | // const file = app.vault.getAbstractFileByPath( 66 | // `${imageFileName}.png` 67 | // ) as TFile; 68 | // console.log({ file }); 69 | 70 | // const node2 = createNode( 71 | // canvas, 72 | // { 73 | // type: "file", 74 | // file, 75 | // size: { 76 | // width: IMAGE_WIDTH, 77 | // height: IMAGE_HEIGHT, 78 | // }, 79 | // }, 80 | // parentNode 81 | // ); 82 | // node2.moveAndResize({ 83 | // size: { 84 | // width: IMAGE_WIDTH, 85 | // height: IMAGE_HEIGHT, 86 | // }, 87 | // }); 88 | 89 | canvas.requestSave(); 90 | }; 91 | -------------------------------------------------------------------------------- /src/actions/canvasNodeMenuActions/advancedCanvas.ts: -------------------------------------------------------------------------------- 1 | import { App, setIcon, setTooltip } from "obsidian"; 2 | import { getTokenLimit, noteGenerator } from "./noteGenerator"; 3 | import { AugmentedCanvasSettings } from "../../settings/AugmentedCanvasSettings"; 4 | import { CanvasNode } from "../../obsidian/canvas-internal"; 5 | import { getResponse } from "../../utils/chatgpt"; 6 | import { getActiveCanvas, getActiveCanvasNodes } from "src/utils"; 7 | 8 | const SYSTEM_PROMPT_QUESTIONS = ` 9 | You must respond in this JSON format: { 10 | "questions": Follow up questions the user could ask based on the chat history, must be an array 11 | } 12 | The questions must be asked in the same language the user used, default to English. 13 | `.trim(); 14 | 15 | export const addAskAIButton = async ( 16 | app: App, 17 | settings: AugmentedCanvasSettings, 18 | menuEl: HTMLElement 19 | ) => { 20 | const buttonEl_AskAI = createEl("button", "clickable-icon gpt-menu-item"); 21 | setTooltip(buttonEl_AskAI, "Ask AI", { 22 | placement: "top", 23 | }); 24 | setIcon(buttonEl_AskAI, "lucide-sparkles"); 25 | menuEl.appendChild(buttonEl_AskAI); 26 | 27 | buttonEl_AskAI.addEventListener("click", async () => { 28 | const { generateNote } = noteGenerator(app, settings); 29 | 30 | await generateNote(); 31 | }); 32 | }; 33 | 34 | export const handleCallGPT_Question = async ( 35 | app: App, 36 | settings: AugmentedCanvasSettings, 37 | node: CanvasNode, 38 | question: string 39 | ) => { 40 | if (node.unknownData.type === "group") { 41 | return; 42 | } 43 | 44 | const { generateNote } = noteGenerator(app, settings); 45 | await generateNote(question); 46 | }; 47 | 48 | export const handleCallGPT_Questions = async ( 49 | app: App, 50 | settings: AugmentedCanvasSettings, 51 | node: CanvasNode 52 | ) => { 53 | const { buildMessages } = noteGenerator(app, settings); 54 | const { messages, tokenCount } = await buildMessages(node, { 55 | systemPrompt: SYSTEM_PROMPT_QUESTIONS, 56 | }); 57 | if (messages.length <= 1) return; 58 | 59 | const gptResponse = await getResponse( 60 | settings.apiKey, 61 | // settings.apiModel, 62 | messages, 63 | { 64 | model: settings.apiModel, 65 | max_tokens: settings.maxResponseTokens || undefined, 66 | // max_tokens: getTokenLimit(settings) - tokenCount - 1, 67 | temperature: settings.temperature, 68 | isJSON: true, 69 | } 70 | ); 71 | 72 | return gptResponse.questions; 73 | }; 74 | 75 | const handleRegenerateResponse = async ( 76 | app: App, 77 | settings: AugmentedCanvasSettings 78 | ) => { 79 | const activeNode = getActiveCanvasNodes(app)![0]; 80 | 81 | // const canvas = getActiveCanvas(app); 82 | 83 | // // @ts-expect-error 84 | // const toNode = activeNode.to.node; 85 | 86 | // console.log({ toNode }); 87 | 88 | // canvas!.removeNode(toNode); 89 | // canvas?.requestSave(); 90 | 91 | const { generateNote } = noteGenerator( 92 | app, 93 | settings, 94 | // @ts-expect-error 95 | activeNode.from.node, 96 | // @ts-expect-error 97 | activeNode.to.node 98 | ); 99 | 100 | await generateNote(); 101 | }; 102 | 103 | export const addRegenerateResponse = async ( 104 | app: App, 105 | settings: AugmentedCanvasSettings, 106 | menuEl: HTMLElement 107 | ) => { 108 | const buttonEl_AskAI = createEl("button", "clickable-icon gpt-menu-item"); 109 | setTooltip(buttonEl_AskAI, "Regenerate response", { 110 | placement: "top", 111 | }); 112 | // TODO 113 | setIcon(buttonEl_AskAI, "lucide-rotate-cw"); 114 | menuEl.appendChild(buttonEl_AskAI); 115 | 116 | buttonEl_AskAI.addEventListener("click", () => 117 | handleRegenerateResponse(app, settings) 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /src/actions/canvasNodeMenuActions/noteGenerator.ts: -------------------------------------------------------------------------------- 1 | import { TiktokenModel, encodingForModel } from "js-tiktoken"; 2 | import { App, ItemView, Notice } from "obsidian"; 3 | import { CanvasNode } from "../../obsidian/canvas-internal"; 4 | import { 5 | CanvasView, 6 | calcHeight, 7 | createNode, 8 | } from "../../obsidian/canvas-patches"; 9 | import { 10 | AugmentedCanvasSettings, 11 | DEFAULT_SETTINGS, 12 | } from "../../settings/AugmentedCanvasSettings"; 13 | // import { Logger } from "./util/logging"; 14 | import { visitNodeAndAncestors } from "../../obsidian/canvasUtil"; 15 | import { readNodeContent } from "../../obsidian/fileUtil"; 16 | import { getResponse, streamResponse } from "../../utils/chatgpt"; 17 | import { CHAT_MODELS, chatModelByName } from "../../openai/models"; 18 | 19 | /** 20 | * Color for assistant notes: 6 == purple 21 | */ 22 | const assistantColor = "6"; 23 | 24 | /** 25 | * Height to use for placeholder note 26 | */ 27 | const placeholderNoteHeight = 60; 28 | 29 | /** 30 | * Height to use for new empty note 31 | */ 32 | const emptyNoteHeight = 100; 33 | 34 | const NOTE_MAX_WIDTH = 400; 35 | export const NOTE_MIN_HEIGHT = 400; 36 | export const NOTE_INCR_HEIGHT_STEP = 150; 37 | 38 | // TODO : remove 39 | const logDebug = (text: any) => null; 40 | 41 | // const SYSTEM_PROMPT2 = ` 42 | // You must respond in this JSON format: { 43 | // "response": Your response, must be in markdown, 44 | // "questions": Follow up questions the user could ask based on your response, must be an array 45 | // } 46 | // The response must be in the same language the user used. 47 | // `.trim(); 48 | 49 | const SYSTEM_PROMPT = ` 50 | You must respond in markdown. 51 | The response must be in the same language the user used. 52 | `.trim(); 53 | 54 | export function noteGenerator( 55 | app: App, 56 | settings: AugmentedCanvasSettings, 57 | fromNode?: CanvasNode, 58 | toNode?: CanvasNode 59 | // logDebug: Logger 60 | ) { 61 | const canCallAI = () => { 62 | // return true; 63 | if (!settings.apiKey) { 64 | new Notice("Please set your OpenAI API key in the plugin settings"); 65 | return false; 66 | } 67 | 68 | return true; 69 | }; 70 | 71 | const getActiveCanvas = () => { 72 | const maybeCanvasView = app.workspace.getActiveViewOfType( 73 | ItemView 74 | ) as CanvasView | null; 75 | return maybeCanvasView ? maybeCanvasView["canvas"] : null; 76 | }; 77 | 78 | const isSystemPromptNode = (text: string) => 79 | text.trim().startsWith("SYSTEM PROMPT"); 80 | 81 | const getSystemPrompt = async (node: CanvasNode) => { 82 | // TODO 83 | let foundPrompt: string | null = null; 84 | 85 | await visitNodeAndAncestors(node, async (n: CanvasNode) => { 86 | const text = await readNodeContent(n); 87 | if (text && isSystemPromptNode(text)) { 88 | foundPrompt = text.replace("SYSTEM PROMPT", "").trim(); 89 | return false; 90 | } else { 91 | return true; 92 | } 93 | }); 94 | 95 | return foundPrompt || settings.systemPrompt; 96 | }; 97 | 98 | const buildMessages = async ( 99 | node: CanvasNode, 100 | { 101 | systemPrompt, 102 | prompt, 103 | }: { 104 | systemPrompt?: string; 105 | prompt?: string; 106 | } = {} 107 | ) => { 108 | // return { messages: [], tokenCount: 0 }; 109 | 110 | const encoding = encodingForModel( 111 | // (settings.apiModel || DEFAULT_SETTINGS.apiModel) as TiktokenModel 112 | "gpt-4" 113 | ); 114 | 115 | const messages: any[] = []; 116 | let tokenCount = 0; 117 | 118 | // Note: We are not checking for system prompt longer than context window. 119 | // That scenario makes no sense, though. 120 | const systemPrompt2 = systemPrompt || (await getSystemPrompt(node)); 121 | if (systemPrompt2) { 122 | tokenCount += encoding.encode(systemPrompt2).length; 123 | } 124 | 125 | const visit = async ( 126 | node: CanvasNode, 127 | depth: number, 128 | edgeLabel?: string 129 | ) => { 130 | if (settings.maxDepth && depth > settings.maxDepth) return false; 131 | 132 | const nodeData = node.getData(); 133 | let nodeText = (await readNodeContent(node))?.trim() || ""; 134 | const inputLimit = getTokenLimit(settings); 135 | 136 | let shouldContinue = true; 137 | 138 | if (nodeText) { 139 | if (isSystemPromptNode(nodeText)) return true; 140 | 141 | let nodeTokens = encoding.encode(nodeText); 142 | let keptNodeTokens: number; 143 | 144 | if (tokenCount + nodeTokens.length > inputLimit) { 145 | // will exceed input limit 146 | 147 | shouldContinue = false; 148 | 149 | // Leaving one token margin, just in case 150 | const keepTokens = nodeTokens.slice( 151 | 0, 152 | inputLimit - tokenCount - 1 153 | // * needed because very large context is a little above 154 | // * should this be a number from settings.maxInput ? 155 | // TODO 156 | // (nodeTokens.length > 100000 ? 20 : 1) 157 | ); 158 | const truncateTextTo = encoding.decode(keepTokens).length; 159 | logDebug( 160 | `Truncating node text from ${nodeText.length} to ${truncateTextTo} characters` 161 | ); 162 | new Notice( 163 | `Truncating node text from ${nodeText.length} to ${truncateTextTo} characters` 164 | ); 165 | nodeText = nodeText.slice(0, truncateTextTo); 166 | keptNodeTokens = keepTokens.length; 167 | } else { 168 | keptNodeTokens = nodeTokens.length; 169 | } 170 | 171 | tokenCount += keptNodeTokens; 172 | 173 | const role: any = 174 | nodeData.chat_role === "assistant" ? "assistant" : "user"; 175 | 176 | if (edgeLabel) { 177 | messages.unshift({ 178 | content: edgeLabel, 179 | role: "user", 180 | }); 181 | } 182 | messages.unshift({ 183 | content: nodeText, 184 | role, 185 | }); 186 | } 187 | 188 | return shouldContinue; 189 | }; 190 | 191 | await visitNodeAndAncestors(node, visit); 192 | 193 | // if (messages.length) { 194 | if (systemPrompt2) 195 | messages.unshift({ 196 | role: "system", 197 | content: systemPrompt2, 198 | }); 199 | // } 200 | 201 | if (prompt) 202 | messages.push({ 203 | role: "user", 204 | content: prompt, 205 | }); 206 | 207 | return { messages, tokenCount }; 208 | // } else { 209 | // return { messages: [], tokenCount: 0 }; 210 | // } 211 | }; 212 | 213 | const generateNote = async (question?: string) => { 214 | if (!canCallAI()) return; 215 | 216 | logDebug("Creating AI note"); 217 | 218 | const canvas = getActiveCanvas(); 219 | if (!canvas) { 220 | logDebug("No active canvas"); 221 | return; 222 | } 223 | // console.log({ canvas }); 224 | 225 | await canvas.requestFrame(); 226 | 227 | let node: CanvasNode; 228 | if (!fromNode) { 229 | const selection = canvas.selection; 230 | if (selection?.size !== 1) return; 231 | const values = Array.from(selection.values()); 232 | node = values[0]; 233 | } else { 234 | node = fromNode; 235 | } 236 | 237 | if (node) { 238 | // Last typed characters might not be applied to note yet 239 | await canvas.requestSave(); 240 | await sleep(200); 241 | 242 | const { messages, tokenCount } = await buildMessages(node, { 243 | prompt: question, 244 | }); 245 | // console.log({ messages }); 246 | if (!messages.length) return; 247 | 248 | let created: CanvasNode; 249 | if (!toNode) { 250 | created = createNode( 251 | canvas, 252 | { 253 | // text: "```loading...```", 254 | text: `\`\`\`Calling AI (${settings.apiModel})...\`\`\``, 255 | size: { height: placeholderNoteHeight }, 256 | }, 257 | node, 258 | { 259 | color: assistantColor, 260 | chat_role: "assistant", 261 | }, 262 | question 263 | ); 264 | } else { 265 | created = toNode; 266 | created.setText( 267 | `\`\`\`Calling AI (${settings.apiModel})...\`\`\`` 268 | ); 269 | } 270 | 271 | new Notice( 272 | `Sending ${messages.length} notes with ${tokenCount} tokens to GPT` 273 | ); 274 | 275 | try { 276 | // logDebug("messages", messages); 277 | 278 | let firstDelta = true; 279 | await streamResponse( 280 | settings.apiKey, 281 | // settings.apiModel, 282 | messages, 283 | { 284 | model: settings.apiModel, 285 | max_tokens: settings.maxResponseTokens || undefined, 286 | // max_tokens: getTokenLimit(settings) - tokenCount - 1, 287 | }, 288 | // { 289 | // max_tokens: settings.maxResponseTokens || undefined, 290 | // temperature: settings.temperature, 291 | // } 292 | (delta?: string) => { 293 | // * Last call 294 | if (!delta) { 295 | // const height = calcHeight({ 296 | // text: created.text, 297 | // parentHeight: node.height, 298 | // }); 299 | // created.moveAndResize({ 300 | // height, 301 | // width: created.width, 302 | // x: created.x, 303 | // y: created.y, 304 | // }); 305 | return; 306 | } 307 | 308 | let newText; 309 | if (firstDelta) { 310 | newText = delta; 311 | firstDelta = false; 312 | 313 | created.moveAndResize({ 314 | height: NOTE_MIN_HEIGHT, 315 | width: created.width, 316 | x: created.x, 317 | y: created.y, 318 | }); 319 | } else { 320 | const height = calcHeight({ 321 | text: created.text, 322 | // parentHeight: node.height, 323 | }); 324 | if (height > created.height) { 325 | created.moveAndResize({ 326 | height: 327 | created.height + NOTE_INCR_HEIGHT_STEP, 328 | width: created.width, 329 | x: created.x, 330 | y: created.y, 331 | }); 332 | } 333 | newText = created.text + delta; 334 | } 335 | created.setText(newText); 336 | } 337 | ); 338 | 339 | // if (generated == null) { 340 | // new Notice(`Empty or unreadable response from GPT`); 341 | // canvas.removeNode(created); 342 | // return; 343 | // } 344 | 345 | // * Update Node 346 | // created.setText(generated.response); 347 | // const nodeData = created.getData(); 348 | // created.setData({ 349 | // ...nodeData, 350 | // questions: generated.questions, 351 | // }); 352 | // const height = calcHeight({ 353 | // text: generated.response, 354 | // parentHeight: node.height, 355 | // }); 356 | // created.moveAndResize({ 357 | // height, 358 | // width: created.width, 359 | // x: created.x, 360 | // y: created.y, 361 | // }); 362 | 363 | // const selectedNoteId = 364 | // canvas.selection?.size === 1 365 | // ? Array.from(canvas.selection.values())?.[0]?.id 366 | // : undefined; 367 | 368 | // if (selectedNoteId === node?.id || selectedNoteId == null) { 369 | // // If the user has not changed selection, select the created node 370 | // canvas.selectOnly(created, false /* startEditing */); 371 | // } 372 | } catch (error) { 373 | new Notice(`Error calling GPT: ${error.message || error}`); 374 | if (!toNode) { 375 | canvas.removeNode(created); 376 | } 377 | } 378 | 379 | await canvas.requestSave(); 380 | } 381 | }; 382 | 383 | // return { nextNote, generateNote }; 384 | return { generateNote, buildMessages }; 385 | } 386 | 387 | export function getTokenLimit(settings: AugmentedCanvasSettings) { 388 | const model = 389 | chatModelByName(settings.apiModel) || CHAT_MODELS.GPT_4_1106_PREVIEW; 390 | const tokenLimit = settings.maxInputTokens 391 | ? Math.min(settings.maxInputTokens, model.tokenLimit) 392 | : model.tokenLimit; 393 | 394 | // console.log({ settings, tokenLimit }); 395 | return tokenLimit; 396 | } 397 | -------------------------------------------------------------------------------- /src/actions/commands/insertSystemPrompt.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice } from "obsidian"; 2 | import { calcHeight, createNode } from "src/obsidian/canvas-patches"; 3 | import { SystemPrompt } from "src/settings/AugmentedCanvasSettings"; 4 | import { getActiveCanvas } from "src/utils"; 5 | 6 | export const insertSystemPrompt = (app: App, systemPrompt: SystemPrompt) => { 7 | new Notice(`Selected ${systemPrompt.act}`); 8 | 9 | const canvas = getActiveCanvas(app); 10 | if (!canvas) return; 11 | 12 | const text = ` 13 | SYSTEM PROMPT 14 | 15 | ${systemPrompt.prompt.trim()} 16 | `.trim(); 17 | 18 | const NODE_WIDTH = 800; 19 | const NODE_HEIGHT = 300; 20 | const newNode = createNode(canvas, { 21 | pos: { 22 | // @ts-expect-error 23 | x: canvas.x - NODE_WIDTH / 2, 24 | // @ts-expect-error 25 | y: canvas.y - NODE_HEIGHT / 2, 26 | }, 27 | // position: "left", 28 | size: { 29 | height: calcHeight({ 30 | // parentHeight: NODE_HEIGHT, 31 | text, 32 | }), 33 | width: NODE_WIDTH, 34 | }, 35 | text, 36 | focus: false, 37 | }); 38 | // canvas.menu.menuEl.append(new MenuItem()) 39 | }; 40 | -------------------------------------------------------------------------------- /src/actions/commands/relevantQuestions.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice } from "obsidian"; 2 | import { AugmentedCanvasSettings } from "../../settings/AugmentedCanvasSettings"; 3 | import { getFilesContent } from "../../obsidian/fileUtil"; 4 | import { getResponse } from "../../utils/chatgpt"; 5 | import { createCanvasGroup } from "../../utils"; 6 | 7 | const RELEVANT_QUESTION_SYSTEM_PROMPT = ` 8 | There must be 6 questions. 9 | 10 | You must respond in this JSON format: { 11 | "questions": The questions 12 | } 13 | 14 | You must respond in the language the user used. 15 | `.trim(); 16 | 17 | export const handleAddRelevantQuestions = async ( 18 | app: App, 19 | settings: AugmentedCanvasSettings 20 | ) => { 21 | new Notice("Generating relevant questions..."); 22 | 23 | const files = await app.vault.getMarkdownFiles(); 24 | 25 | const sortedFiles = files.sort((a, b) => b.stat.mtime - a.stat.mtime); 26 | 27 | const actualFiles = sortedFiles.slice( 28 | 0, 29 | settings.insertRelevantQuestionsFilesCount 30 | ); 31 | console.log({ actualFiles }); 32 | 33 | const filesContent = await getFilesContent(app, actualFiles); 34 | 35 | const gptResponse = await getResponse( 36 | settings.apiKey, 37 | [ 38 | { 39 | role: "system", 40 | content: ` 41 | ${settings.relevantQuestionsSystemPrompt} 42 | ${RELEVANT_QUESTION_SYSTEM_PROMPT} 43 | `, 44 | }, 45 | { 46 | role: "user", 47 | content: filesContent, 48 | }, 49 | ], 50 | { isJSON: true } 51 | ); 52 | // console.log({ gptResponse }); 53 | 54 | await createCanvasGroup(app, "Questions", gptResponse.questions); 55 | 56 | new Notice("Generating relevant questions done successfully."); 57 | }; 58 | -------------------------------------------------------------------------------- /src/actions/commands/runPromptFolder.ts: -------------------------------------------------------------------------------- 1 | import { App, Notice, TFolder } from "obsidian"; 2 | import { ChatCompletionMessageParam } from "openai/resources"; 3 | import { calcHeight, createNode } from "src/obsidian/canvas-patches"; 4 | import { 5 | AugmentedCanvasSettings, 6 | SystemPrompt, 7 | } from "src/settings/AugmentedCanvasSettings"; 8 | import { getActiveCanvas } from "src/utils"; 9 | import { streamResponse } from "src/utils/chatgpt"; 10 | import { 11 | NOTE_INCR_HEIGHT_STEP, 12 | NOTE_MIN_HEIGHT, 13 | } from "../canvasNodeMenuActions/noteGenerator"; 14 | import { readFolderMarkdownContent } from "src/obsidian/fileUtil"; 15 | 16 | export const runPromptFolder = async ( 17 | app: App, 18 | settings: AugmentedCanvasSettings, 19 | systemPrompt: SystemPrompt, 20 | folder: TFolder 21 | ) => { 22 | const canvas = getActiveCanvas(app); 23 | if (!canvas) return; 24 | 25 | const NODE_WIDTH = 800; 26 | const NODE_HEIGHT = 300; 27 | const text = `\`\`\`Calling AI (${settings.apiModel})...\`\`\``; 28 | const created = createNode(canvas, { 29 | pos: { 30 | // @ts-expect-error 31 | x: canvas.x - NODE_WIDTH / 2, 32 | // @ts-expect-error 33 | y: canvas.y - NODE_HEIGHT / 2, 34 | }, 35 | // position: "left", 36 | size: { 37 | height: calcHeight({ 38 | // parentHeight: NODE_HEIGHT, 39 | text, 40 | }), 41 | width: NODE_WIDTH, 42 | }, 43 | text, 44 | focus: false, 45 | }); 46 | // canvas.menu.menuEl.append(new MenuItem()) 47 | 48 | const folderContentText = await readFolderMarkdownContent(app, folder); 49 | 50 | const messages: ChatCompletionMessageParam[] = [ 51 | { 52 | role: "system", 53 | content: systemPrompt.prompt, 54 | }, 55 | { 56 | role: "user", 57 | content: folderContentText, 58 | }, 59 | ]; 60 | 61 | let firstDelta = true; 62 | await streamResponse( 63 | settings.apiKey, 64 | // settings.apiModel, 65 | messages, 66 | { 67 | model: settings.apiModel, 68 | max_tokens: settings.maxResponseTokens || undefined, 69 | // max_tokens: getTokenLimit(settings) - tokenCount - 1, 70 | }, 71 | (delta?: string) => { 72 | // * Last call 73 | if (!delta) { 74 | return; 75 | } 76 | 77 | let newText; 78 | if (firstDelta) { 79 | newText = delta; 80 | firstDelta = false; 81 | 82 | created.moveAndResize({ 83 | height: NOTE_MIN_HEIGHT, 84 | width: created.width, 85 | x: created.x, 86 | y: created.y, 87 | }); 88 | } else { 89 | const height = calcHeight({ 90 | text: created.text, 91 | }); 92 | if (height > created.height) { 93 | created.moveAndResize({ 94 | height: created.height + NOTE_INCR_HEIGHT_STEP, 95 | width: created.width, 96 | x: created.x, 97 | y: created.y, 98 | }); 99 | } 100 | newText = created.text + delta; 101 | } 102 | created.setText(newText); 103 | } 104 | ); 105 | 106 | canvas.requestSave(); 107 | }; 108 | -------------------------------------------------------------------------------- /src/actions/commands/websiteContent.ts: -------------------------------------------------------------------------------- 1 | import { App } from "obsidian"; 2 | import { AugmentedCanvasSettings } from "src/settings/AugmentedCanvasSettings"; 3 | import { getWebsiteContent } from "src/utils/websiteContentUtils"; 4 | 5 | export const insertWebsiteContent = ( 6 | app: App, 7 | settings: AugmentedCanvasSettings, 8 | url: string 9 | ) => { 10 | // const { textContent } = getWebsiteContent(url); 11 | }; 12 | -------------------------------------------------------------------------------- /src/actions/commands/youtubeCaptions.ts: -------------------------------------------------------------------------------- 1 | import { App } from "obsidian"; 2 | import { AugmentedCanvasSettings } from "src/settings/AugmentedCanvasSettings"; 3 | import { getYouTubeVideoId } from "src/utils"; 4 | 5 | import { google } from "googleapis"; 6 | 7 | async function getVideoSubtitles( 8 | settings: AugmentedCanvasSettings, 9 | videoId: string 10 | ): Promise { 11 | // // TODO Convert to Oauth 12 | // const youtube = google.youtube({ 13 | // version: "v3", 14 | // auth: settings.youtubeApiKey, // Replace with your API key 15 | // }); 16 | // try { 17 | // const response = await youtube.captions.list({ 18 | // part: ["snippet"], 19 | // videoId: videoId, 20 | // }); 21 | // console.log({ response }); 22 | // const items = response.data.items; 23 | // if (items) { 24 | // const subtitles = []; 25 | // for await (const caption of items) { 26 | // console.log({ caption }); 27 | // try { 28 | // const response = await youtube.captions.download( 29 | // { 30 | // id: caption.id!, 31 | // tfmt: "ttml", // This specifies the format of the captions file. Options are 'ttml' or 'vtt' for example. 32 | // }, 33 | // { 34 | // responseType: "text", 35 | // } 36 | // ); 37 | // // The caption content will be in the response body as a string 38 | // subtitles.push(response.data as string); 39 | // } catch (error) { 40 | // console.error("Error downloading caption:", error); 41 | // } 42 | // } 43 | // return subtitles; 44 | // } 45 | // return []; 46 | // } catch (error) { 47 | // console.error("Error fetching video captions:", error); 48 | // return []; 49 | // } 50 | } 51 | 52 | export const runYoutubeCaptions = async ( 53 | app: App, 54 | settings: AugmentedCanvasSettings, 55 | videoUrl: string 56 | ) => { 57 | // const videoId = getYouTubeVideoId(videoUrl); 58 | // console.log({ videoId }); 59 | // if (!videoId) return; 60 | // const subtitles = await getVideoSubtitles(settings, videoId); 61 | // console.log({ subtitles }); 62 | }; 63 | -------------------------------------------------------------------------------- /src/actions/menuPatches/noteMenuPatch.ts: -------------------------------------------------------------------------------- 1 | import { App, Canvas, Menu } from "obsidian"; 2 | import { CanvasNode } from "../../obsidian/canvas-internal"; 3 | import { AugmentedCanvasSettings } from "../../settings/AugmentedCanvasSettings"; 4 | import { CustomQuestionModal } from "../../modals/CustomQuestionModal"; 5 | import { 6 | handleCallGPT_Question, 7 | handleCallGPT_Questions, 8 | } from "../canvasNodeMenuActions/advancedCanvas"; 9 | import { handleCanvasMenu_Loading, handleCanvasMenu_Loaded } from "./utils"; 10 | 11 | export const handlePatchNoteMenu = async ( 12 | buttonEl_AskQuestions: HTMLButtonElement, 13 | menuEl: HTMLElement, 14 | { 15 | app, 16 | settings, 17 | canvas, 18 | }: { 19 | app: App; 20 | settings: AugmentedCanvasSettings; 21 | canvas: Canvas; 22 | } 23 | ) => { 24 | const pos = buttonEl_AskQuestions.getBoundingClientRect(); 25 | if (!buttonEl_AskQuestions.hasClass("has-active-menu")) { 26 | buttonEl_AskQuestions.toggleClass("has-active-menu", true); 27 | const menu = new Menu(); 28 | // const containingNodes = 29 | // this.canvas.getContainingNodes( 30 | // this.selection.bbox 31 | // ); 32 | 33 | const node = ( 34 | Array.from(canvas.selection)?.first() 35 | ); 36 | if (!node) return; 37 | 38 | handleCanvasMenu_Loading( 39 | menu, 40 | node.unknownData.questions, 41 | async (question: string) => { 42 | if (!question) { 43 | } else { 44 | handleCallGPT_Question( 45 | app, 46 | settings, 47 | // @ts-expect-error 48 | Array.from(canvas.selection)?.first(), 49 | question 50 | ); 51 | } 52 | } 53 | ); 54 | menu.setParentElement(menuEl).showAtPosition({ 55 | x: pos.x, 56 | y: pos.bottom, 57 | width: pos.width, 58 | overlap: true, 59 | }); 60 | 61 | if (node.unknownData.questions) return; 62 | 63 | const questions = await handleCallGPT_Questions(app, settings, node); 64 | if (!questions) return; 65 | node.unknownData.questions = questions; 66 | 67 | menu.hide(); 68 | 69 | const menu2 = new Menu(); 70 | 71 | handleCanvasMenu_Loaded(menu2, questions, async (question?: string) => { 72 | if (!question) { 73 | let modal = new CustomQuestionModal( 74 | app, 75 | (question2: string) => { 76 | handleCallGPT_Question( 77 | app, 78 | settings, 79 | // @ts-expect-error 80 | Array.from(canvas.selection)?.first()!, 81 | question2 82 | ); 83 | // Handle the input 84 | } 85 | ); 86 | modal.open(); 87 | } else { 88 | handleCallGPT_Question( 89 | app, 90 | settings, 91 | // @ts-expect-error 92 | Array.from(canvas.selection)?.first(), 93 | question 94 | ); 95 | } 96 | }); 97 | menu2.setParentElement(menuEl).showAtPosition({ 98 | x: pos.x, 99 | y: pos.bottom, 100 | width: pos.width, 101 | overlap: true, 102 | }); 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /src/actions/menuPatches/utils.ts: -------------------------------------------------------------------------------- 1 | import { Menu, MenuItem } from "obsidian"; 2 | 3 | // TODO : ask GPT and add subMenu items 4 | export const handleCanvasMenu_Loading = async ( 5 | subMenu: Menu, 6 | questions?: string[], 7 | callback?: (question?: string) => Promise 8 | ) => { 9 | if (questions) { 10 | if (questions.length === 0) { 11 | subMenu.addItem((item: MenuItem) => { 12 | item 13 | // .setIcon("fold-vertical") 14 | .setTitle("No questions"); 15 | }); 16 | } else { 17 | questions.forEach((question: string) => 18 | subMenu.addItem((item: MenuItem) => { 19 | item 20 | // .setIcon("fold-vertical") 21 | .setTitle(question) 22 | .onClick(async () => { 23 | callback && (await callback(question)); 24 | }); 25 | }) 26 | ); 27 | } 28 | } else { 29 | subMenu.addItem((item: MenuItem) => { 30 | item 31 | // .setIcon("fold-vertical") 32 | .setTitle("loading..."); 33 | }); 34 | } 35 | }; 36 | 37 | // TODO : ask GPT and add subMenu items 38 | export const handleCanvasMenu_Loaded = async ( 39 | subMenu: Menu, 40 | questions: string[], 41 | callback: (question?: string) => Promise 42 | ) => { 43 | // subMenu. 44 | if (questions.length === 0) { 45 | subMenu.addItem((item: MenuItem) => { 46 | item 47 | // .setIcon("fold-vertical") 48 | .setTitle("No questions"); 49 | }); 50 | } else { 51 | questions.forEach((question: string) => 52 | subMenu.addItem((item: MenuItem) => { 53 | item 54 | // .setIcon("fold-vertical") 55 | .setTitle(question) 56 | .onClick(async () => { 57 | await callback(question); 58 | }); 59 | }) 60 | ); 61 | } 62 | 63 | return subMenu; 64 | }; 65 | -------------------------------------------------------------------------------- /src/logDebug.ts: -------------------------------------------------------------------------------- 1 | import AugmentedCanvasPlugin from "./AugmentedCanvasPlugin"; 2 | import { AugmentedCanvasSettings } from "./settings/AugmentedCanvasSettings"; 3 | 4 | let settings: AugmentedCanvasSettings | null = null; 5 | 6 | export const initLogDebug = (settings2: AugmentedCanvasSettings) => { 7 | // console.log({ settings2 }); 8 | settings = settings2; 9 | }; 10 | 11 | // @ts-expect-error 12 | export const logDebug = (...params) => { 13 | // console.log({ settings }) 14 | settings?.debug && console.log(...params); 15 | }; 16 | -------------------------------------------------------------------------------- /src/obsidian/canvas-internal.d.ts: -------------------------------------------------------------------------------- 1 | import { App, TFile } from "obsidian"; 2 | import { AllCanvasNodeData, CanvasData } from "obsidian/canvas"; 3 | 4 | export interface CanvasNode { 5 | id: string; 6 | app: App; 7 | canvas: Canvas; 8 | child: Partial; 9 | color: string; 10 | containerEl: HTMLElement; 11 | containerBlockerEl: HTMLElement; 12 | contentEl: HTMLElement; 13 | destroyted: boolean; 14 | height: number; 15 | initialized: boolean; 16 | isContentMounted: boolean; 17 | isEditing: boolean; 18 | nodeEl: HTMLElement; 19 | placeholderEl: HTMLElement; 20 | renderedZIndex: number; 21 | resizeDirty: boolean; 22 | text: string; 23 | unknownData: Record; 24 | width: number; 25 | x: number; 26 | y: number; 27 | zIndex: number; 28 | subpath?: string; 29 | convertToFile(): Promise; 30 | focus(): void; 31 | getData(): AllCanvasNodeData; 32 | initialize(): void; 33 | moveAndResize(options: MoveAndResizeOptions): void; 34 | render(): void; 35 | setData(data: Partial): void; 36 | setText(text: string): Promise; 37 | showMenu(): void; 38 | startEditing(): void; 39 | } 40 | 41 | export interface MoveAndResizeOptions { 42 | x?: number; 43 | y?: number; 44 | width?: number; 45 | height?: number; 46 | } 47 | 48 | export interface CanvasEdge { 49 | from: { 50 | node: CanvasNode; 51 | }; 52 | to: { 53 | node: CanvasNode; 54 | }; 55 | } 56 | 57 | export interface Canvas { 58 | edges: CanvasEdge[]; 59 | selection: Set; 60 | nodes: CanvasNode[]; 61 | wrapperEl: HTMLElement | null; 62 | addNode(node: CanvasNode): void; 63 | createTextNode(options: CreateNodeOptions): CanvasNode; 64 | deselectAll(): void; 65 | getData(): CanvasData; 66 | getEdgesForNode(node: CanvasNode): CanvasEdge[]; 67 | importData(data: { nodes: object[]; edges: object[] }): void; 68 | removeNode(node: CanvasNode): void; 69 | requestFrame(): Promise; 70 | requestSave(): Promise; 71 | selectOnly(node: CanvasNode, startEditing: boolean): void; 72 | } 73 | 74 | export interface CreateNodeOptions { 75 | type?: string; 76 | text?: string; 77 | file?: TFile; 78 | pos?: { x: number; y: number }; 79 | position?: "left" | "right" | "top" | "bottom"; 80 | size?: { height?: number; width?: number }; 81 | focus?: boolean; 82 | } 83 | -------------------------------------------------------------------------------- /src/obsidian/canvas-patches.ts: -------------------------------------------------------------------------------- 1 | import { ItemView } from "obsidian"; 2 | import { AllCanvasNodeData } from "obsidian/canvas"; 3 | import { randomHexString } from "../utils"; 4 | import { Canvas, CanvasNode, CreateNodeOptions } from "./canvas-internal"; 5 | 6 | export interface CanvasEdgeIntermediate { 7 | fromOrTo: string; 8 | side: string; 9 | node: CanvasElement; 10 | } 11 | 12 | interface CanvasElement { 13 | id: string; 14 | } 15 | 16 | export type CanvasView = ItemView & { 17 | canvas: Canvas; 18 | }; 19 | 20 | /** 21 | * Minimum width for new notes 22 | */ 23 | const minWidth = 360; 24 | 25 | /** 26 | * Assumed pixel width per character 27 | */ 28 | const pxPerChar = 5; 29 | 30 | /** 31 | * Assumed pixel height per line 32 | */ 33 | const pxPerLine = 28; 34 | 35 | /** 36 | * Assumed height of top + bottom text area padding 37 | */ 38 | const textPaddingHeight = 12; 39 | 40 | /** 41 | * Margin between new notes 42 | */ 43 | const newNoteMargin = 60; 44 | const newNoteMarginWithLabel = 110; 45 | 46 | /** 47 | * Min height of new notes 48 | */ 49 | const minHeight = 60; 50 | 51 | /** 52 | * Choose height for generated note based on text length and parent height. 53 | * For notes beyond a few lines, the note will have scroll bar. 54 | * Not a precise science, just something that is not surprising. 55 | */ 56 | // export const calcHeight = (options: { parentHeight: number; text: string }) => { 57 | export const calcHeight = (options: { text: string }) => { 58 | const calcTextHeight = Math.round( 59 | textPaddingHeight + 60 | (pxPerLine * options.text.length) / (minWidth / pxPerChar) 61 | ); 62 | return calcTextHeight; 63 | // return Math.max(options.parentHeight, calcTextHeight); 64 | }; 65 | 66 | const DEFAULT_NODE_WIDTH = 400; 67 | const DEFAULT_NODE_HEIGHT = DEFAULT_NODE_WIDTH * (1024 / 1792) + 20; 68 | 69 | /** 70 | * Create new node as descendant from the parent node. 71 | * Align and offset relative to siblings. 72 | */ 73 | export const createNode = ( 74 | canvas: Canvas, 75 | nodeOptions: CreateNodeOptions, 76 | parentNode?: CanvasNode, 77 | nodeData?: Partial, 78 | edgeLabel?: string 79 | ) => { 80 | if (!canvas) { 81 | throw new Error("Invalid arguments"); 82 | } 83 | 84 | const { text } = nodeOptions; 85 | 86 | const width = parentNode 87 | ? nodeOptions?.size?.width || Math.max(minWidth, parentNode?.width) 88 | : DEFAULT_NODE_WIDTH; 89 | 90 | const height = text 91 | ? parentNode 92 | ? nodeOptions?.size?.height || 93 | Math.max( 94 | minHeight, 95 | parentNode && 96 | calcHeight({ 97 | text, 98 | // parentHeight: parentNode.height 99 | }) 100 | ) 101 | : DEFAULT_NODE_HEIGHT 102 | : undefined; 103 | 104 | // @ts-expect-error 105 | let x = canvas.x - width / 2; 106 | // @ts-expect-error 107 | let y = canvas.y - height / 2; 108 | 109 | if (parentNode) { 110 | const siblings = 111 | parent && 112 | canvas 113 | .getEdgesForNode(parentNode) 114 | .filter((n) => n.from.node.id == parentNode.id) 115 | .map((e) => e.to.node); 116 | 117 | // Failsafe leftmost value. 118 | const farLeft = parentNode.y - parentNode.width * 5; 119 | const siblingsRight = siblings?.length 120 | ? siblings.reduce( 121 | (right, sib) => Math.max(right, sib.x + sib.width), 122 | farLeft 123 | ) 124 | : undefined; 125 | const priorSibling = siblings[siblings.length - 1]; 126 | 127 | // Position left at right of prior sibling, otherwise aligned with parent 128 | x = 129 | siblingsRight != null 130 | ? siblingsRight + newNoteMargin 131 | : parentNode.x; 132 | 133 | // Position top at prior sibling top, otherwise offset below parent 134 | y = 135 | (priorSibling 136 | ? priorSibling.y 137 | : parentNode.y + 138 | parentNode.height + 139 | (edgeLabel ? newNoteMarginWithLabel : newNoteMargin)) + 140 | // Using position=left, y value is treated as vertical center 141 | height! * 0.5; 142 | } 143 | 144 | const newNode = 145 | nodeOptions.type === "file" 146 | ? // @ts-expect-error 147 | canvas.createFileNode({ 148 | file: nodeOptions.file, 149 | pos: { x, y }, 150 | // // position: "left", 151 | // size: { height, width }, 152 | // focus: false, 153 | }) 154 | : canvas.createTextNode({ 155 | pos: { x, y }, 156 | position: "left", 157 | size: { height, width }, 158 | text, 159 | focus: false, 160 | }); 161 | 162 | if (nodeData) { 163 | newNode.setData(nodeData); 164 | } 165 | 166 | canvas.deselectAll(); 167 | canvas.addNode(newNode); 168 | 169 | if (parentNode) { 170 | addEdge( 171 | canvas, 172 | randomHexString(16), 173 | { 174 | fromOrTo: "from", 175 | side: "bottom", 176 | node: parentNode, 177 | }, 178 | { 179 | fromOrTo: "to", 180 | side: "top", 181 | node: newNode, 182 | }, 183 | edgeLabel, 184 | { 185 | isGenerated: true, 186 | } 187 | ); 188 | } 189 | 190 | return newNode; 191 | }; 192 | 193 | /** 194 | * Add edge entry to canvas. 195 | */ 196 | export const addEdge = ( 197 | canvas: Canvas, 198 | edgeID: string, 199 | fromEdge: CanvasEdgeIntermediate, 200 | toEdge: CanvasEdgeIntermediate, 201 | label?: string, 202 | edgeData?: { 203 | isGenerated: boolean; 204 | } 205 | ) => { 206 | if (!canvas) return; 207 | 208 | const data = canvas.getData(); 209 | 210 | if (!data) return; 211 | 212 | canvas.importData({ 213 | edges: [ 214 | ...data.edges, 215 | { 216 | ...edgeData, 217 | id: edgeID, 218 | fromNode: fromEdge.node.id, 219 | fromSide: fromEdge.side, 220 | toNode: toEdge.node.id, 221 | toSide: toEdge.side, 222 | label, 223 | }, 224 | ], 225 | nodes: data.nodes, 226 | }); 227 | 228 | canvas.requestFrame(); 229 | }; 230 | 231 | /** 232 | * Trap exception and write to console.error. 233 | */ 234 | export function trapError(fn: (...params: unknown[]) => T) { 235 | return (...params: unknown[]) => { 236 | try { 237 | return fn(...params); 238 | } catch (e) { 239 | console.error(e); 240 | } 241 | }; 242 | } 243 | -------------------------------------------------------------------------------- /src/obsidian/canvasUtil.ts: -------------------------------------------------------------------------------- 1 | import { CanvasNode } from "src/obsidian/canvas-internal"; 2 | 3 | export type HasId = { 4 | id: string; 5 | }; 6 | 7 | export type NodeVisitor = ( 8 | node: HasId, 9 | depth: number, 10 | edgeLabel?: string 11 | ) => Promise; 12 | 13 | /** 14 | * Get parents for canvas node 15 | */ 16 | export function nodeParents(node: CanvasNode) { 17 | const canvas = node.canvas; 18 | const nodes = canvas 19 | .getEdgesForNode(node) 20 | .filter((edge) => edge.to.node.id === node.id) 21 | .map((edge) => ({ 22 | node: edge.from.node, 23 | // @ts-expect-error 24 | edgeLabel: edge.label, 25 | })); 26 | // Left-to-right for node ordering 27 | nodes.sort((a, b) => b.node.x - a.node.x); 28 | return nodes; 29 | } 30 | 31 | /** 32 | * Visit node and ancestors breadth-first 33 | */ 34 | export async function visitNodeAndAncestors( 35 | start: { id: string }, 36 | visitor: NodeVisitor, 37 | getNodeParents: ( 38 | node: HasId 39 | ) => { node: HasId; edgeLabel: string }[] = nodeParents 40 | ) { 41 | const visited = new Set(); 42 | const queue: { node: HasId; depth: number; edgeLabel?: string }[] = [ 43 | { node: start, depth: 0, edgeLabel: undefined }, 44 | ]; 45 | 46 | while (queue.length > 0) { 47 | const { node: currentNode, depth, edgeLabel } = queue.shift()!; 48 | if (visited.has(currentNode.id)) { 49 | continue; 50 | } 51 | 52 | const shouldContinue = await visitor(currentNode, depth, edgeLabel); 53 | if (!shouldContinue) { 54 | break; 55 | } 56 | 57 | visited.add(currentNode.id); 58 | 59 | const parents = getNodeParents(currentNode); 60 | for (const parent of parents) { 61 | if (!visited.has(parent.node.id)) { 62 | queue.push({ 63 | node: parent.node, 64 | depth: depth + 1, 65 | edgeLabel: parent.edgeLabel, 66 | }); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/obsidian/fileUtil.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | TAbstractFile, 4 | TFile, 5 | TFolder, 6 | loadPdfJs, 7 | resolveSubpath, 8 | } from "obsidian"; 9 | import { Canvas, CanvasNode, CreateNodeOptions } from "./canvas-internal"; 10 | import { AugmentedCanvasSettings } from "src/settings/AugmentedCanvasSettings"; 11 | 12 | export async function readFileContent( 13 | app: App, 14 | file: TFile, 15 | subpath?: string | undefined 16 | ) { 17 | // TODO: remove frontmatter 18 | const body = await app.vault.read(file); 19 | 20 | if (subpath) { 21 | const cache = app.metadataCache.getFileCache(file); 22 | if (cache) { 23 | const resolved = resolveSubpath(cache, subpath); 24 | if (!resolved) { 25 | console.warn("Failed to get subpath", { file, subpath }); 26 | return body; 27 | } 28 | if (resolved.start || resolved.end) { 29 | const subText = body.slice( 30 | resolved.start.offset, 31 | resolved.end?.offset 32 | ); 33 | if (subText) { 34 | return subText; 35 | } else { 36 | console.warn("Failed to get subpath", { file, subpath }); 37 | return body; 38 | } 39 | } 40 | } 41 | } 42 | 43 | return body; 44 | } 45 | 46 | const pdfToMarkdown = async (app: App, file: TFile) => { 47 | const pdfjsLib = await loadPdfJs(); 48 | 49 | const pdfBuffer = await app.vault.readBinary(file); 50 | const loadingTask = pdfjsLib.getDocument({ data: pdfBuffer }); 51 | const pdf = await loadingTask.promise; 52 | 53 | const ebookTitle = file 54 | .path!.split("/") 55 | .pop()! 56 | .replace(/\.pdf$/i, ""); 57 | 58 | let markdownContent = `# ${ebookTitle} 59 | 60 | `; 61 | 62 | for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { 63 | const page = await pdf.getPage(pageNum); 64 | const textContent = await page.getTextContent(); 65 | 66 | let pageText = textContent.items 67 | .map((item: { str: string }) => item.str) 68 | .join(" "); 69 | 70 | // Here you would need to enhance the logic to convert the text into Markdown. 71 | // For example, you could detect headers, lists, tables, etc., and apply the appropriate Markdown formatting. 72 | // This can get quite complex depending on the structure and layout of the original PDF. 73 | 74 | // Add a page break after each page's content. 75 | markdownContent += pageText + "\n\n---\n\n"; 76 | } 77 | 78 | return markdownContent; 79 | }; 80 | 81 | const epubToMarkdown = async (app: App, file: TFile) => { 82 | // TODO 83 | return ""; 84 | }; 85 | 86 | const readDifferentExtensionFileContent = async (app: App, file: TFile) => { 87 | // console.log({ file }); 88 | switch (file.extension) { 89 | case "md": 90 | const body = await app.vault.cachedRead(file); 91 | return `## ${file.basename}\n${body}`; 92 | 93 | case "pdf": 94 | return pdfToMarkdown(app, file); 95 | 96 | case "epub": 97 | return epubToMarkdown(app, file); 98 | 99 | default: 100 | break; 101 | } 102 | }; 103 | 104 | export async function readNodeContent(node: CanvasNode) { 105 | const app = node.app; 106 | const nodeData = node.getData(); 107 | switch (nodeData.type) { 108 | case "text": 109 | return nodeData.text; 110 | case "file": 111 | const file = app.vault.getAbstractFileByPath(nodeData.file); 112 | if (file instanceof TFile) { 113 | if (node.subpath) { 114 | return await readFileContent(app, file, nodeData.subpath); 115 | } else { 116 | return readDifferentExtensionFileContent(app, file); 117 | } 118 | } else { 119 | console.debug("Cannot read from file type", file); 120 | } 121 | } 122 | } 123 | 124 | export const getFilesContent = async (app: App, files: TFile[]) => { 125 | let content = ""; 126 | 127 | for (const file of files) { 128 | const fileContent = await readFileContent(app, file); 129 | 130 | content += `# ${file.basename} 131 | 132 | ${fileContent} 133 | 134 | `; 135 | } 136 | 137 | return content; 138 | }; 139 | 140 | export const updateNodeAndSave = async ( 141 | canvas: Canvas, 142 | node: CanvasNode, 143 | // TODO: only accepts .text .size not working (is it Obsidian API?) 144 | nodeOptions: CreateNodeOptions 145 | ) => { 146 | // console.log({ nodeOptions }); 147 | // node.setText(nodeOptions.text); 148 | // @ts-expect-error 149 | node.setData(nodeOptions); 150 | await canvas.requestSave(); 151 | }; 152 | 153 | export const generateFileName = (prefix: string = "file"): string => { 154 | const now = new Date(); 155 | const year = now.getUTCFullYear(); 156 | const month = (now.getUTCMonth() + 1).toString().padStart(2, "0"); 157 | const day = now.getUTCDate().toString().padStart(2, "0"); 158 | const hours = now.getUTCHours().toString().padStart(2, "0"); 159 | const minutes = now.getUTCMinutes().toString().padStart(2, "0"); 160 | const seconds = now.getUTCSeconds().toString().padStart(2, "0"); 161 | 162 | return `${prefix}_${year}-${month}-${day}_${hours}-${minutes}-${seconds}`; 163 | }; 164 | 165 | /* 166 | * Will read canvas node content || md note content 167 | * TODO add backlinks reading 168 | */ 169 | export const cachedReadFile = async (app: App, file: TFile) => { 170 | if (file.path.endsWith(".canvas")) { 171 | const canvasJson = JSON.parse(await app.vault.cachedRead(file)); 172 | console.log({ canvasJson }); 173 | 174 | const nodesContent: string[] = []; 175 | 176 | if (canvasJson.nodes) { 177 | for await (const node of canvasJson.nodes) { 178 | if (node.type === "text") { 179 | nodesContent.push(node.text!); 180 | } else if (node.type === "file") { 181 | nodesContent.push( 182 | await cachedReadFile( 183 | app, 184 | app.vault.getAbstractFileByPath(node.file!) as TFile 185 | ) 186 | ); 187 | } 188 | } 189 | } 190 | 191 | // console.log({ canvas: { file, nodesContent } }); 192 | 193 | return nodesContent.join("\n\n"); 194 | } else { 195 | return await app.vault.cachedRead(file); 196 | } 197 | }; 198 | 199 | // TODO : if there is a canvas which link to a file in the same folder then the folder can be read two times 200 | export const readFolderMarkdownContent = async (app: App, folder: TFolder) => { 201 | // console.log({ folder }); 202 | 203 | const filesContent: string[] = []; 204 | for await (const fileOrFolder of folder.children) { 205 | if (fileOrFolder instanceof TFile) { 206 | // TODO special parsing for .canvas 207 | filesContent.push( 208 | ` 209 | # ${fileOrFolder.path} 210 | 211 | ${await cachedReadFile(app, fileOrFolder)} 212 | `.trim() 213 | ); 214 | } else { 215 | filesContent.push( 216 | `${await readFolderMarkdownContent( 217 | app, 218 | fileOrFolder as TFolder 219 | )}` 220 | ); 221 | } 222 | } 223 | 224 | return filesContent.join("\n\n"); 225 | }; 226 | -------------------------------------------------------------------------------- /src/obsidian/imageUtils.ts: -------------------------------------------------------------------------------- 1 | import { App, base64ToArrayBuffer } from "obsidian"; 2 | import { AugmentedCanvasSettings } from "src/settings/AugmentedCanvasSettings"; 3 | 4 | const writeImageToFile = async ( 5 | app: App, 6 | imageBuffer: ArrayBuffer, 7 | imagePath: string 8 | ): Promise => { 9 | try { 10 | const fileAdapter = app.vault.adapter; // Get the current file adapter 11 | // TODO : bind to settings attachments path or fallback to settings.imagePath 12 | 13 | // Write the array buffer to the vault 14 | await fileAdapter.writeBinary(imagePath, new Uint8Array(imageBuffer)); 15 | console.log("Image saved successfully."); 16 | } catch (error) { 17 | console.error("Error saving the image:", error); 18 | } 19 | }; 20 | 21 | // const base64ToArrayBuffer = (base64: string): ArrayBuffer => { 22 | // const binaryString: string = window.atob(base64); 23 | // const len: number = binaryString.length; 24 | // const bytes: Uint8Array = new Uint8Array(len); 25 | // for (let i = 0; i < len; i++) { 26 | // bytes[i] = binaryString.charCodeAt(i); 27 | // } 28 | // return bytes.buffer; 29 | // }; 30 | 31 | export const saveBase64Image = async ( 32 | app: App, 33 | imagePath: string, 34 | base64Image: string 35 | ): Promise => { 36 | // Remove 'data:image/png;base64,' if present 37 | const base64Data: string = base64Image.split(",")[1] || base64Image; 38 | 39 | // Convert base64 to array buffer 40 | const imageBuffer: ArrayBuffer = base64ToArrayBuffer(base64Data); 41 | 42 | // Save to file 43 | await writeImageToFile(app, imageBuffer, imagePath); 44 | }; 45 | -------------------------------------------------------------------------------- /src/openai/models.ts: -------------------------------------------------------------------------------- 1 | export const CHAT_MODELS = { 2 | // GPT35: { 3 | // name: "gpt-3.5-turbo", 4 | // tokenLimit: 4096, 5 | // }, 6 | // GPT35_16K: { 7 | // name: "gpt-3.5-turbo-16k", 8 | // tokenLimit: 16384, 9 | // }, 10 | // GPT4: { 11 | // name: "gpt-4", 12 | // tokenLimit: 8000, 13 | // }, 14 | GPT_4_0: { 15 | name: "gpt-4o", 16 | tokenLimit: 128000, 17 | }, 18 | GPT_4_0_MINI: { 19 | name: "gpt-4o-mini", 20 | tokenLimit: 128000, 21 | }, 22 | GPT_4_1106_PREVIEW: { 23 | name: "gpt-4-1106-preview", 24 | tokenLimit: 128000, 25 | }, 26 | // GPT4_32K: { 27 | // name: "gpt-4-32k", 28 | // tokenLimit: 32768, 29 | // }, 30 | }; 31 | 32 | export const IMAGE_MODELS = { 33 | DALL_E_2: { 34 | name: "dall-e-2", 35 | // tokenLimit: 128000, 36 | }, 37 | DALL_E_3: { 38 | name: "dall-e-3", 39 | // tokenLimit: 128000, 40 | }, 41 | }; 42 | export function chatModelByName(name: string) { 43 | return Object.values(CHAT_MODELS).find((model) => model.name === name); 44 | } 45 | -------------------------------------------------------------------------------- /src/settings/AugmentedCanvasSettings.ts: -------------------------------------------------------------------------------- 1 | import { FuseIndex } from "fuse.js"; 2 | import { CHAT_MODELS, IMAGE_MODELS } from "src/openai/models"; 3 | 4 | export interface SystemPrompt { 5 | id: number; 6 | act: string; 7 | prompt: string; 8 | } 9 | 10 | export interface AugmentedCanvasSettings { 11 | /** 12 | * The API key to use when making requests 13 | */ 14 | apiKey: string; 15 | 16 | /** 17 | * The GPT model to use 18 | */ 19 | apiModel: string; 20 | 21 | /** 22 | * The temperature to use when generating responses (0-2). 0 means no randomness. 23 | */ 24 | temperature: number; 25 | 26 | /** 27 | * The system prompt sent with each request to the API 28 | */ 29 | systemPrompt: string; 30 | 31 | /** 32 | * Enable debug output in the console 33 | */ 34 | debug: boolean; 35 | 36 | /** 37 | * The maximum number of tokens to send (up to model limit). 0 means as many as possible. 38 | */ 39 | maxInputTokens: number; 40 | 41 | /** 42 | * The maximum number of tokens to return from the API. 0 means no limit. (A token is about 4 characters). 43 | */ 44 | maxResponseTokens: number; 45 | 46 | /** 47 | * The maximum depth of ancestor notes to include. 0 means no limit. 48 | */ 49 | maxDepth: number; 50 | 51 | /** 52 | * System prompt list fetch from github 53 | */ 54 | systemPrompts: SystemPrompt[]; 55 | 56 | /** 57 | * User system prompts 58 | */ 59 | userSystemPrompts: SystemPrompt[]; 60 | 61 | /** 62 | * System prompt used to generate flashcards file 63 | */ 64 | flashcardsSystemPrompt: string; 65 | 66 | /** 67 | * System prompt used to generate flashcards file 68 | */ 69 | insertRelevantQuestionsFilesCount: number; 70 | 71 | /** 72 | * System prompt used to generate flashcards file 73 | */ 74 | relevantQuestionsSystemPrompt: string; 75 | 76 | /** 77 | * Model used for image generation 78 | */ 79 | imageModel: string; 80 | 81 | /** 82 | * The path where generated images are stored 83 | */ 84 | imagesPath?: string; 85 | 86 | /** 87 | * The Youtube API Key 88 | */ 89 | youtubeApiKey: string; 90 | } 91 | // export const DEFAULT_SYSTEM_PROMPT = ` 92 | // You are a critical-thinking assistant bot. 93 | // Consider the intent of my questions before responding. 94 | // Do not restate my information unless I ask for it. 95 | // Do not include caveats or disclaimers. 96 | // Use step-by-step reasoning. Be brief. 97 | // `.trim(); 98 | 99 | const DEFAULT_SYSTEM_PROMPT = ` 100 | You must respond in markdown. 101 | The response must be in the same language the user used, default to english. 102 | `.trim(); 103 | 104 | const FLASHCARDS_SYSTEM_PROMPT = ` 105 | You will create a file containing flashcards. 106 | 107 | The front of the flashcard must be a question. 108 | 109 | The question must not give the answer, If the question is too precise, ask a more general question. 110 | 111 | If there is a list in the text given by the user. Start by creating a flashcard asking about this list. 112 | 113 | The filename, can be written with spaces, must not contain the word "flashcard", must tell the subjects of the flashcards. 114 | `.trim(); 115 | 116 | const RELEVANT_QUESTION_SYSTEM_PROMPT = ` 117 | You will ask relevant questions based on the user input. 118 | 119 | These questions must be opened questions. 120 | 121 | Priories questions that connect different topics together. 122 | `.trim(); 123 | 124 | export const DEFAULT_SETTINGS: AugmentedCanvasSettings = { 125 | apiKey: "", 126 | apiModel: CHAT_MODELS.GPT_4_0.name, 127 | temperature: 1, 128 | systemPrompt: DEFAULT_SYSTEM_PROMPT, 129 | debug: false, 130 | maxInputTokens: 0, 131 | maxResponseTokens: 0, 132 | maxDepth: 0, 133 | systemPrompts: [], 134 | userSystemPrompts: [], 135 | flashcardsSystemPrompt: FLASHCARDS_SYSTEM_PROMPT, 136 | insertRelevantQuestionsFilesCount: 10, 137 | relevantQuestionsSystemPrompt: RELEVANT_QUESTION_SYSTEM_PROMPT, 138 | imageModel: IMAGE_MODELS.DALL_E_3.name, 139 | imagesPath: undefined, 140 | youtubeApiKey: "", 141 | }; 142 | 143 | export function getModels() { 144 | return Object.entries(CHAT_MODELS).map(([, value]) => value.name); 145 | } 146 | 147 | export function getImageModels() { 148 | return Object.entries(IMAGE_MODELS).map(([, value]) => value.name); 149 | } 150 | -------------------------------------------------------------------------------- /src/settings/SettingsTab.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | ButtonComponent, 4 | Notice, 5 | PluginSettingTab, 6 | Setting, 7 | TextAreaComponent, 8 | TextComponent, 9 | } from "obsidian"; 10 | import ChatStreamPlugin from "./../AugmentedCanvasPlugin"; 11 | import { 12 | SystemPrompt, 13 | getImageModels, 14 | getModels, 15 | } from "./AugmentedCanvasSettings"; 16 | import { initLogDebug } from "src/logDebug"; 17 | 18 | export class SettingsTab extends PluginSettingTab { 19 | plugin: ChatStreamPlugin; 20 | 21 | constructor(app: App, plugin: ChatStreamPlugin) { 22 | super(app, plugin); 23 | this.plugin = plugin; 24 | } 25 | 26 | display(): void { 27 | const { containerEl } = this; 28 | 29 | containerEl.empty(); 30 | 31 | new Setting(containerEl) 32 | .setName("Model") 33 | .setDesc("Select the GPT model to use.") 34 | .addDropdown((cb) => { 35 | getModels().forEach((model) => { 36 | cb.addOption(model, model); 37 | }); 38 | cb.setValue(this.plugin.settings.apiModel); 39 | cb.onChange(async (value) => { 40 | this.plugin.settings.apiModel = value; 41 | await this.plugin.saveSettings(); 42 | }); 43 | }); 44 | 45 | new Setting(containerEl) 46 | .setName("Image Model") 47 | .setDesc("Select the GPT model to generate images.") 48 | .addDropdown((cb) => { 49 | getImageModels().forEach((model) => { 50 | cb.addOption(model, model); 51 | }); 52 | cb.setValue(this.plugin.settings.imageModel); 53 | cb.onChange(async (value) => { 54 | this.plugin.settings.imageModel = value; 55 | await this.plugin.saveSettings(); 56 | }); 57 | }); 58 | 59 | new Setting(containerEl) 60 | .setName("API key") 61 | .setDesc( 62 | "The API key to use when making requests - Get from OpenAI" 63 | ) 64 | .addText((text) => { 65 | text.inputEl.type = "password"; 66 | text.setPlaceholder("API Key") 67 | .setValue(this.plugin.settings.apiKey) 68 | .onChange(async (value) => { 69 | this.plugin.settings.apiKey = value; 70 | await this.plugin.saveSettings(); 71 | }); 72 | }); 73 | 74 | new Setting(containerEl) 75 | .setName("Youtube API key") 76 | .setDesc("The Youtube API key used to fetch captions") 77 | .addText((text) => { 78 | text.inputEl.type = "password"; 79 | text.setPlaceholder("API Key") 80 | .setValue(this.plugin.settings.youtubeApiKey) 81 | .onChange(async (value) => { 82 | this.plugin.settings.youtubeApiKey = value; 83 | await this.plugin.saveSettings(); 84 | }); 85 | }); 86 | 87 | new Setting(containerEl) 88 | .setName("Default system prompt") 89 | .setDesc( 90 | `The system prompt sent with each request to the API. \n(Note: you can override this by beginning a note stream with a note starting 'SYSTEM PROMPT'. The remaining content of that note will be used as system prompt.)` 91 | ) 92 | .addTextArea((component) => { 93 | component.inputEl.rows = 6; 94 | // component.inputEl.style.width = "300px"; 95 | // component.inputEl.style.fontSize = "10px"; 96 | component.inputEl.addClass("augmented-canvas-settings-prompt"); 97 | component.setValue(this.plugin.settings.systemPrompt); 98 | component.onChange(async (value) => { 99 | this.plugin.settings.systemPrompt = value; 100 | await this.plugin.saveSettings(); 101 | }); 102 | }); 103 | 104 | this.displaySystemPromptsSettings(containerEl); 105 | 106 | new Setting(containerEl) 107 | .setName("Flashcards system prompt") 108 | .setDesc(`The system prompt used to generate the flashcards file.`) 109 | .addTextArea((component) => { 110 | component.inputEl.rows = 6; 111 | // component.inputEl.style.width = "300px"; 112 | // component.inputEl.style.fontSize = "10px"; 113 | component.inputEl.addClass("augmented-canvas-settings-prompt"); 114 | component.setValue(this.plugin.settings.flashcardsSystemPrompt); 115 | component.onChange(async (value) => { 116 | this.plugin.settings.flashcardsSystemPrompt = value; 117 | await this.plugin.saveSettings(); 118 | }); 119 | }); 120 | 121 | new Setting(containerEl) 122 | .setName("Relevant questions system prompt") 123 | .setDesc( 124 | `The system prompt used to generate relevant questions for the command "Insert relevant questions".` 125 | ) 126 | .addTextArea((component) => { 127 | component.inputEl.rows = 6; 128 | // component.inputEl.style.width = "300px"; 129 | // component.inputEl.style.fontSize = "10px"; 130 | component.inputEl.addClass("augmented-canvas-settings-prompt"); 131 | component.setValue( 132 | this.plugin.settings.relevantQuestionsSystemPrompt 133 | ); 134 | component.onChange(async (value) => { 135 | this.plugin.settings.relevantQuestionsSystemPrompt = value; 136 | await this.plugin.saveSettings(); 137 | }); 138 | }); 139 | 140 | new Setting(containerEl) 141 | .setName("Insert relevant questions files count") 142 | .setDesc( 143 | 'The number of files that are taken into account by the "Insert relevant questions" command.' 144 | ) 145 | .addText((text) => 146 | text 147 | .setValue( 148 | this.plugin.settings.insertRelevantQuestionsFilesCount.toString() 149 | ) 150 | .onChange(async (value) => { 151 | const parsed = parseInt(value); 152 | if (!isNaN(parsed)) { 153 | this.plugin.settings.insertRelevantQuestionsFilesCount = 154 | parsed; 155 | await this.plugin.saveSettings(); 156 | } 157 | }) 158 | ); 159 | 160 | new Setting(containerEl) 161 | .setName("Max input tokens") 162 | .setDesc( 163 | "The maximum number of tokens to send (within model limit). 0 means as many as possible" 164 | ) 165 | .addText((text) => 166 | text 167 | .setValue(this.plugin.settings.maxInputTokens.toString()) 168 | .onChange(async (value) => { 169 | const parsed = parseInt(value); 170 | if (!isNaN(parsed)) { 171 | this.plugin.settings.maxInputTokens = parsed; 172 | await this.plugin.saveSettings(); 173 | } 174 | }) 175 | ); 176 | 177 | new Setting(containerEl) 178 | .setName("Max response tokens") 179 | .setDesc( 180 | "The maximum number of tokens to return from the API. 0 means no limit. (A token is about 4 characters)." 181 | ) 182 | .addText((text) => 183 | text 184 | .setValue(this.plugin.settings.maxResponseTokens.toString()) 185 | .onChange(async (value) => { 186 | const parsed = parseInt(value); 187 | if (!isNaN(parsed)) { 188 | this.plugin.settings.maxResponseTokens = parsed; 189 | await this.plugin.saveSettings(); 190 | } 191 | }) 192 | ); 193 | 194 | new Setting(containerEl) 195 | .setName("Max depth") 196 | .setDesc( 197 | "The maximum depth of ancestor notes to include. 0 means no limit." 198 | ) 199 | .addText((text) => 200 | text 201 | .setValue(this.plugin.settings.maxDepth.toString()) 202 | .onChange(async (value) => { 203 | const parsed = parseInt(value); 204 | if (!isNaN(parsed)) { 205 | this.plugin.settings.maxDepth = parsed; 206 | await this.plugin.saveSettings(); 207 | } 208 | }) 209 | ); 210 | 211 | new Setting(containerEl) 212 | .setName("Temperature") 213 | .setDesc("Sampling temperature (0-2). 0 means no randomness.") 214 | .addText((text) => 215 | text 216 | .setValue(this.plugin.settings.temperature.toString()) 217 | .onChange(async (value) => { 218 | const parsed = parseFloat(value); 219 | if (!isNaN(parsed) && parsed >= 0 && parsed <= 2) { 220 | this.plugin.settings.temperature = parsed; 221 | await this.plugin.saveSettings(); 222 | } 223 | }) 224 | ); 225 | 226 | // new Setting(containerEl) 227 | // .setName("API URL") 228 | // .setDesc( 229 | // "The chat completions URL to use. You probably won't need to change this." 230 | // ) 231 | // .addText((text) => { 232 | // text.inputEl.style.width = "300px"; 233 | // text.setPlaceholder("API URL") 234 | // .setValue(this.plugin.settings.apiUrl) 235 | // .onChange(async (value) => { 236 | // this.plugin.settings.apiUrl = value; 237 | // await this.plugin.saveSettings(); 238 | // }); 239 | // }); 240 | 241 | new Setting(containerEl) 242 | .setName("Debug output") 243 | .setDesc("Enable debug output in the console") 244 | .addToggle((component) => { 245 | component 246 | .setValue(this.plugin.settings.debug) 247 | .onChange(async (value) => { 248 | this.plugin.settings.debug = value; 249 | await this.plugin.saveSettings(); 250 | initLogDebug(this.plugin.settings); 251 | }); 252 | }); 253 | } 254 | 255 | displaySystemPromptsSettings(containerEl: HTMLElement): void { 256 | const setting = new Setting(containerEl); 257 | 258 | setting 259 | .setName("Add system prompts") 260 | .setClass("augmented-canvas-setting-item") 261 | .setDesc( 262 | `Create new highlight colors by providing a color name and using the color picker to set the hex code value. Don't forget to save the color before exiting the color picker. Drag and drop the highlight color to change the order for your highlighter component.` 263 | ); 264 | 265 | const nameInput = new TextComponent(setting.controlEl); 266 | nameInput.setPlaceholder("Name"); 267 | // colorInput.inputEl.addClass("highlighter-settings-color"); 268 | 269 | let promptInput: TextAreaComponent; 270 | setting.addTextArea((component) => { 271 | component.inputEl.rows = 6; 272 | // component.inputEl.style.width = "300px"; 273 | // component.inputEl.style.fontSize = "10px"; 274 | component.setPlaceholder("Prompt"); 275 | component.inputEl.addClass("augmented-canvas-settings-prompt"); 276 | promptInput = component; 277 | }); 278 | 279 | setting.addButton((button) => { 280 | button 281 | .setIcon("lucide-plus") 282 | .setTooltip("Add") 283 | .onClick(async (buttonEl: any) => { 284 | let name = nameInput.inputEl.value; 285 | const prompt = promptInput.inputEl.value; 286 | 287 | // console.log({ name, prompt }); 288 | 289 | if (!name || !prompt) { 290 | name && !prompt 291 | ? new Notice("Prompt missing") 292 | : !name && prompt 293 | ? new Notice("Name missing") 294 | : new Notice("Values missing"); // else 295 | return; 296 | } 297 | 298 | // * Handles multiple with the same name 299 | // if ( 300 | // this.plugin.settings.systemPrompts.filter( 301 | // (systemPrompt: SystemPrompt) => 302 | // systemPrompt.act === name 303 | // ).length 304 | // ) { 305 | // name += " 2"; 306 | // } 307 | // let count = 3; 308 | // while ( 309 | // this.plugin.settings.systemPrompts.filter( 310 | // (systemPrompt: SystemPrompt) => 311 | // systemPrompt.act === name 312 | // ).length 313 | // ) { 314 | // name = name.slice(0, -2) + " " + count; 315 | // count++; 316 | // } 317 | 318 | if ( 319 | !this.plugin.settings.systemPrompts.filter( 320 | (systemPrompt: SystemPrompt) => 321 | systemPrompt.act === name 322 | ).length && 323 | !this.plugin.settings.userSystemPrompts.filter( 324 | (systemPrompt: SystemPrompt) => 325 | systemPrompt.act === name 326 | ).length 327 | ) { 328 | this.plugin.settings.userSystemPrompts.push({ 329 | id: 330 | this.plugin.settings.systemPrompts.length + 331 | this.plugin.settings.userSystemPrompts.length, 332 | act: name, 333 | prompt, 334 | }); 335 | await this.plugin.saveSettings(); 336 | this.display(); 337 | } else { 338 | buttonEl.stopImmediatePropagation(); 339 | new Notice("This system prompt name already exists"); 340 | } 341 | }); 342 | }); 343 | 344 | const listContainer = containerEl.createEl("div", { 345 | cls: "augmented-canvas-list-container", 346 | }); 347 | 348 | this.plugin.settings.userSystemPrompts.forEach( 349 | (systemPrompt: SystemPrompt) => { 350 | const listElement = listContainer.createEl("div", { 351 | cls: "augmented-canvas-list-element", 352 | }); 353 | 354 | const nameInput = new TextComponent(listElement); 355 | nameInput.setValue(systemPrompt.act); 356 | 357 | const promptInput = new TextAreaComponent(listElement); 358 | promptInput.inputEl.addClass( 359 | "augmented-canvas-settings-prompt" 360 | ); 361 | promptInput.setValue(systemPrompt.prompt); 362 | 363 | const buttonSave = new ButtonComponent(listElement); 364 | buttonSave 365 | .setIcon("lucide-save") 366 | .setTooltip("Save") 367 | .onClick(async (buttonEl: any) => { 368 | let name = nameInput.inputEl.value; 369 | const prompt = promptInput.inputEl.value; 370 | 371 | // console.log({ name, prompt }); 372 | this.plugin.settings.userSystemPrompts = 373 | this.plugin.settings.userSystemPrompts.map( 374 | (systemPrompt2: SystemPrompt) => 375 | systemPrompt2.id === systemPrompt.id 376 | ? { 377 | ...systemPrompt2, 378 | act: name, 379 | prompt, 380 | } 381 | : systemPrompt2 382 | ); 383 | await this.plugin.saveSettings(); 384 | this.display(); 385 | new Notice("System prompt updated"); 386 | }); 387 | 388 | const buttonDelete = new ButtonComponent(listElement); 389 | buttonDelete 390 | .setIcon("lucide-trash") 391 | .setTooltip("Delete") 392 | .onClick(async (buttonEl: any) => { 393 | let name = nameInput.inputEl.value; 394 | const prompt = promptInput.inputEl.value; 395 | 396 | // console.log({ name, prompt }); 397 | this.plugin.settings.userSystemPrompts = 398 | this.plugin.settings.userSystemPrompts.filter( 399 | (systemPrompt2: SystemPrompt) => 400 | systemPrompt2.id !== systemPrompt.id 401 | ); 402 | await this.plugin.saveSettings(); 403 | this.display(); 404 | new Notice("System prompt deleted"); 405 | }); 406 | } 407 | ); 408 | } 409 | } 410 | 411 | export default SettingsTab; 412 | -------------------------------------------------------------------------------- /src/types/canvas.d.ts: -------------------------------------------------------------------------------- 1 | export type CanvasNodeType = 'link' | 'file' | 'text' | 'group'; 2 | export type CanvasDirection = 3 | 'bottomright' 4 | | 'bottomleft' 5 | | 'topright' 6 | | 'topleft' 7 | | 'right' 8 | | 'left' 9 | | 'top' 10 | | 'bottom'; 11 | 12 | export interface CanvasNodeUnknownData { 13 | id: string; 14 | type: CanvasNodeType; 15 | collapsed: boolean; 16 | 17 | [key: string]: any; 18 | } 19 | -------------------------------------------------------------------------------- /src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | import { CanvasNode, Component } from "obsidian"; 2 | import { CanvasNodeType } from "./canvas"; 3 | 4 | interface HeaderComponent extends Component { 5 | checkNodeType(): CanvasNodeType; 6 | 7 | initHeader(): void; 8 | 9 | initContent(): void; 10 | 11 | initTypeIcon(): void; 12 | 13 | setIconOrContent(action: string): void; 14 | 15 | setCollapsed(collapsed: boolean): void; 16 | 17 | toggleCollapsed(): Promise; 18 | 19 | refreshHistory(): void; 20 | 21 | updateNode(): void; 22 | 23 | updateNodesInGroup(): void; 24 | 25 | updateEdges(): void; 26 | 27 | updateEdgesInGroup(node: CanvasNode, triggerCollapsed?: boolean): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/types/event.d.ts: -------------------------------------------------------------------------------- 1 | type CanvasCollapseEvt = "collapse" | "expand"; 2 | 3 | -------------------------------------------------------------------------------- /src/types/obsidian.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | import { App, Component, EventRef, ItemView, TFile } from "obsidian"; 3 | import { CanvasDirection, CanvasNodeUnknownData } from "./canvas"; 4 | import { CanvasData } from "obsidian/canvas"; 5 | 6 | declare module "obsidian" { 7 | 8 | type CanvasNodeID = string; 9 | type CanvasEdgeID = string; 10 | 11 | interface Menu { 12 | setParentElement(parent: HTMLElement): Menu; 13 | } 14 | 15 | interface MenuItem { 16 | setSubmenu(): Menu; 17 | } 18 | 19 | interface WorkspaceLeaf { 20 | rebuildView(): void; 21 | } 22 | 23 | interface Workspace { 24 | on( 25 | name: CanvasCollapseEvt, 26 | cb: ( 27 | view: ItemView, 28 | nodeID?: string[] | undefined, 29 | ) => any, 30 | ): EventRef; 31 | 32 | on( 33 | name: "canvas:selection-menu", 34 | cb: ( 35 | menu: Menu, 36 | canvas: Canvas, 37 | ) => any, 38 | ): EventRef; 39 | 40 | on( 41 | name: "canvas:node-menu", 42 | cb: ( 43 | menu: Menu, 44 | node: CanvasNode, 45 | ) => any, 46 | ): EventRef; 47 | 48 | on( 49 | name: "collapse-node:plugin-disabled", 50 | cb: () => any, 51 | ): EventRef; 52 | 53 | on( 54 | name: "collapse-node:patched-canvas", 55 | cb: () => any, 56 | ): EventRef; 57 | } 58 | 59 | interface Workspace { 60 | trigger( 61 | name: CanvasCollapseEvt, 62 | view: ItemView, 63 | nodeID?: string[] | undefined, 64 | ): void; 65 | 66 | trigger( 67 | name: "collapse-node:plugin-disabled", 68 | ): void; 69 | 70 | trigger( 71 | name: "collapse-node:patched-canvas", 72 | ): void; 73 | } 74 | 75 | interface CanvasView extends View { 76 | canvas: Canvas; 77 | } 78 | 79 | interface Canvas { 80 | readonly: boolean; 81 | 82 | x: number; 83 | y: number; 84 | nodes: Map; 85 | edges: Map; 86 | nodeInteractionLayer: CanvasInteractionLayer; 87 | selection: Set; 88 | 89 | menu: CanvasMenu; 90 | 91 | wrapperEl: HTMLElement; 92 | 93 | history: any; 94 | requestPushHistory: any; 95 | nodeIndex: any; 96 | 97 | requestSave(save?: boolean, triggerBySelf?: boolean): void; 98 | 99 | getData(): CanvasData; 100 | 101 | setData(data: CanvasData): void; 102 | 103 | getEdgesForNode(node: CanvasNode): CanvasEdge[]; 104 | 105 | getContainingNodes(coords: CanvasCoords): CanvasNode[]; 106 | 107 | deselectAll(): void; 108 | 109 | select(nodes: CanvasNode): void; 110 | 111 | requestFrame(): void; 112 | } 113 | 114 | interface ICanvasData { 115 | nodes: CanvasNode[]; 116 | edges: CanvasEdge[]; 117 | } 118 | 119 | interface CanvasMenu { 120 | containerEl: HTMLElement; 121 | menuEl: HTMLElement; 122 | canvas: Canvas; 123 | selection: CanvasSelection; 124 | 125 | render(): void; 126 | 127 | updateZIndex(): void; 128 | } 129 | 130 | interface CanvasSelection { 131 | selectionEl: HTMLElement; 132 | resizerEls: HTMLElement; 133 | canvas: Canvas; 134 | bbox: CanvasCoords | undefined; 135 | 136 | render(): void; 137 | 138 | hide(): void; 139 | 140 | onResizePointerDown(e: PointerEvent, direction: CanvasDirection): void; 141 | 142 | update(bbox: CanvasCoords): void; 143 | 144 | } 145 | 146 | interface CanvasInteractionLayer { 147 | interactionEl: HTMLElement; 148 | canvas: Canvas; 149 | target: CanvasNode | null; 150 | 151 | render(): void; 152 | 153 | setTarget(target: CanvasNode | null): void; 154 | } 155 | 156 | interface CanvasNode { 157 | id: CanvasNodeID; 158 | 159 | x: number; 160 | y: number; 161 | width: number; 162 | height: number; 163 | zIndex: number; 164 | bbox: CanvasCoords; 165 | unknownData: CanvasNodeUnknownData; 166 | renderedZIndex: number; 167 | 168 | headerComponent: Component; 169 | 170 | nodeEl: HTMLElement; 171 | labelEl: HTMLElement; 172 | contentEl: HTMLElement; 173 | containerEl: HTMLElement; 174 | 175 | canvas: Canvas; 176 | app: App; 177 | 178 | getBBox(containing?: boolean): CanvasCoords; 179 | 180 | render(): void; 181 | } 182 | 183 | interface CanvasTextNode extends CanvasNode { 184 | text: string; 185 | } 186 | 187 | interface CanvasFileNode extends CanvasNode { 188 | file: TFile; 189 | } 190 | 191 | interface CanvasLinkNode extends CanvasNode { 192 | url: string; 193 | } 194 | 195 | interface CanvasGroupNode extends CanvasNode { 196 | label: string; 197 | } 198 | 199 | interface CanvasEdge { 200 | id: CanvasEdgeID; 201 | 202 | label: string | undefined; 203 | lineStartGroupEl: SVGGElement; 204 | lineEndGroupEl: SVGGElement; 205 | lineGroupEl: SVGGElement; 206 | 207 | path: { 208 | display: SVGPathElement; 209 | interaction: SVGPathElement; 210 | } 211 | 212 | canvas: Canvas; 213 | bbox: CanvasCoords; 214 | 215 | unknownData: CanvasNodeUnknownData; 216 | } 217 | 218 | interface CanvasCoords { 219 | maxX: number; 220 | maxY: number; 221 | minX: number; 222 | minY: number; 223 | } 224 | } 225 | 226 | 227 | 228 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Canvas, 4 | CanvasCoords, 5 | ItemView, 6 | Menu, 7 | MenuItem, 8 | TFile, 9 | CanvasGroupNode, 10 | } from "obsidian"; 11 | import { CanvasView, createNode } from "./obsidian/canvas-patches"; 12 | import { readFileContent, readNodeContent } from "./obsidian/fileUtil"; 13 | import { CanvasNode } from "./obsidian/canvas-internal"; 14 | import { AugmentedCanvasSettings } from "./settings/AugmentedCanvasSettings"; 15 | // from obsidian-chat-stream 16 | 17 | /** 18 | * Generate a string of random hexadecimal chars 19 | */ 20 | export const randomHexString = (len: number) => { 21 | const t = []; 22 | for (let n = 0; n < len; n++) { 23 | t.push(((16 * Math.random()) | 0).toString(16)); 24 | } 25 | return t.join(""); 26 | }; 27 | 28 | export const getActiveCanvas = (app: App) => { 29 | const maybeCanvasView = app.workspace.getActiveViewOfType( 30 | ItemView 31 | ) as CanvasView | null; 32 | return maybeCanvasView ? maybeCanvasView["canvas"] : null; 33 | }; 34 | 35 | export const createCanvasGroup = ( 36 | app: App, 37 | groupName: string, 38 | notesContents: string[] 39 | ) => { 40 | const canvas = getActiveCanvas(app); 41 | if (!canvas) return; 42 | 43 | const NOTE_WIDTH = 500; 44 | const NOTE_HEIGHT = 150; 45 | const NOTE_GAP = 20; 46 | 47 | const NOTES_BY_ROW = 3; 48 | 49 | let startPos = { 50 | // @ts-expect-error 51 | x: canvas.x - ((NOTE_WIDTH + NOTE_GAP) * NOTES_BY_ROW) / 2, 52 | // @ts-expect-error 53 | y: canvas.y - ((NOTE_HEIGHT + NOTE_GAP) * 2) / 2, 54 | }; 55 | 56 | // @ts-expect-error 57 | const newGroup: CanvasGroupNode = canvas.createGroupNode({ 58 | // TODO : does not work 59 | label: groupName, 60 | pos: { 61 | x: startPos.x - NOTE_GAP, 62 | y: startPos.y - NOTE_GAP, 63 | }, 64 | size: { 65 | width: NOTES_BY_ROW * (NOTE_WIDTH + NOTE_GAP) + NOTE_GAP, 66 | height: (NOTE_HEIGHT + NOTE_GAP) * 2 + NOTE_GAP, 67 | }, 68 | }); 69 | newGroup.label = groupName; 70 | newGroup.labelEl.setText(groupName); 71 | 72 | let countRow = 0; 73 | let countColumn = 0; 74 | for (const noteContent of notesContents) { 75 | const newNode = canvas.createTextNode({ 76 | text: noteContent, 77 | pos: { 78 | x: startPos.x + countRow * (NOTE_WIDTH + NOTE_GAP), 79 | y: startPos.y + countColumn * (NOTE_HEIGHT + NOTE_GAP), 80 | }, 81 | size: { 82 | width: NOTE_WIDTH, 83 | height: NOTE_HEIGHT, 84 | }, 85 | }); 86 | canvas.addNode(newNode); 87 | countColumn = 88 | countRow + 1 > NOTES_BY_ROW - 1 ? countColumn + 1 : countColumn; 89 | countRow = countRow + 1 > NOTES_BY_ROW - 1 ? 0 : countRow + 1; 90 | } 91 | 92 | // @ts-expect-error 93 | canvas.addGroup(newGroup); 94 | }; 95 | 96 | export const canvasNodeIsNote = (canvasNode: CanvasNode) => { 97 | // @ts-expect-error 98 | return !canvasNode.from; 99 | }; 100 | 101 | export const getActiveCanvasNodes = (app: App) => { 102 | const canvas = getActiveCanvas(app); 103 | if (!canvas) return; 104 | 105 | return Array.from(canvas.selection)!; 106 | }; 107 | 108 | export const getCanvasActiveNoteText = (app: App) => { 109 | const canvasNodes = getActiveCanvasNodes(app); 110 | if (!canvasNodes || canvasNodes.length !== 1) return; 111 | 112 | const canvasNode = canvasNodes.first()!; 113 | if (!canvasNodeIsNote(canvasNode)) return; 114 | 115 | return readNodeContent(canvasNode); 116 | }; 117 | 118 | // export const addImageToCanvas = (app: App, imageFileName: string) => { 119 | // const canvas = getActiveCanvas(app); 120 | // if (!canvas) return; 121 | 122 | // const parentNode = getActiveCanvasNodes(app)?.[0]; 123 | // if (!parentNode) return; 124 | 125 | // const IMAGE_WIDTH = parentNode.width; 126 | // const IMAGE_HEIGHT = IMAGE_WIDTH * (1024 / 1792) + 20; 127 | 128 | // createNode( 129 | // canvas, 130 | // { 131 | // text: `![[${imageFileName}]]`, 132 | // size: { 133 | // width: IMAGE_WIDTH, 134 | // height: IMAGE_HEIGHT, 135 | // }, 136 | // }, 137 | // parentNode 138 | // ); 139 | 140 | // canvas.requestSave(); 141 | // }; 142 | 143 | export const getImageSaveFolderPath = async ( 144 | app: App, 145 | settings: AugmentedCanvasSettings 146 | ) => { 147 | // @ts-expect-error 148 | const attachments = (await app.vault.getAvailablePathForAttachments()) 149 | .split("/") 150 | .slice(0, -1) 151 | .join("/"); 152 | console.log({ attachments }); 153 | 154 | return attachments; 155 | // // @ts-expect-error 156 | // return settings.imagesPath || app.vault.config.attachmentFolderPath; 157 | }; 158 | 159 | export function getYouTubeVideoId(url: string): string | null { 160 | // This pattern will match the following types of YouTube URLs: 161 | // - http://www.youtube.com/watch?v=VIDEO_ID 162 | // - http://www.youtube.com/watch?v=VIDEO_ID&... 163 | // - http://www.youtube.com/embed/VIDEO_ID 164 | // - http://youtu.be/VIDEO_ID 165 | // The capture group (VIDEO_ID) is the YouTube video ID 166 | const pattern = 167 | /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i; 168 | const match = url.match(pattern); 169 | return match ? match[1] : null; 170 | } 171 | -------------------------------------------------------------------------------- /src/utils/chatgpt.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import { ChatCompletionMessageParam } from "openai/resources"; 3 | import { logDebug } from "src/logDebug"; 4 | 5 | export type Message = { 6 | role: string; 7 | content: string; 8 | }; 9 | 10 | export const streamResponse = async ( 11 | apiKey: string, 12 | // prompt: string, 13 | messages: ChatCompletionMessageParam[], 14 | { 15 | max_tokens, 16 | model, 17 | temperature, 18 | }: { 19 | max_tokens?: number; 20 | model?: string; 21 | temperature?: number; 22 | } = {}, 23 | cb: any 24 | ) => { 25 | logDebug("Calling AI :", { 26 | messages, 27 | model, 28 | max_tokens, 29 | temperature, 30 | isJSON: false, 31 | }); 32 | // console.log({ messages, max_tokens }); 33 | const openai = new OpenAI({ 34 | apiKey: apiKey, 35 | dangerouslyAllowBrowser: true, 36 | }); 37 | 38 | const stream = await openai.chat.completions.create({ 39 | model: model || "gpt-4", 40 | messages, 41 | stream: true, 42 | max_tokens, 43 | temperature, 44 | }); 45 | for await (const chunk of stream) { 46 | logDebug("AI chunk", { chunk }); 47 | // console.log({ completionChoice: chunk.choices[0] }); 48 | cb(chunk.choices[0]?.delta?.content || ""); 49 | } 50 | cb(null); 51 | }; 52 | 53 | export const getResponse = async ( 54 | apiKey: string, 55 | // prompt: string, 56 | messages: ChatCompletionMessageParam[], 57 | { 58 | model, 59 | max_tokens, 60 | temperature, 61 | isJSON, 62 | }: { 63 | model?: string; 64 | max_tokens?: number; 65 | temperature?: number; 66 | isJSON?: boolean; 67 | } = {} 68 | ) => { 69 | logDebug("Calling AI :", { 70 | messages, 71 | model, 72 | max_tokens, 73 | temperature, 74 | isJSON, 75 | }); 76 | 77 | const openai = new OpenAI({ 78 | apiKey: apiKey, 79 | dangerouslyAllowBrowser: true, 80 | }); 81 | 82 | // const totalTokens = 83 | // openaiMessages.reduce( 84 | // (total, message) => total + (message.content?.length || 0), 85 | // 0 86 | // ) * 2; 87 | // console.log({ totalTokens }); 88 | 89 | const completion = await openai.chat.completions.create({ 90 | // model: "gpt-3.5-turbo", 91 | model: model || "gpt-4-1106-preview", 92 | messages, 93 | max_tokens, 94 | temperature, 95 | response_format: { type: isJSON ? "json_object" : "text" }, 96 | }); 97 | 98 | logDebug("AI response", { completion }); 99 | return isJSON 100 | ? JSON.parse(completion.choices[0].message!.content!) 101 | : completion.choices[0].message!.content!; 102 | }; 103 | 104 | let count = 0; 105 | export const createImage = async ( 106 | apiKey: string, 107 | prompt: string, 108 | { 109 | isVertical = false, 110 | model, 111 | }: { 112 | isVertical?: boolean; 113 | model?: string; 114 | } 115 | ) => { 116 | logDebug("Calling AI :", { 117 | prompt, 118 | model, 119 | }); 120 | const openai = new OpenAI({ 121 | apiKey: apiKey, 122 | dangerouslyAllowBrowser: true, 123 | }); 124 | 125 | count++; 126 | // console.log({ createImage: { prompt, count } }); 127 | const response = await openai.images.generate({ 128 | model: model || "dall-e-3", 129 | prompt, 130 | n: 1, 131 | size: isVertical ? "1024x1792" : "1792x1024", 132 | response_format: "b64_json", 133 | }); 134 | logDebug("AI response", { response }); 135 | // console.log({ responseImg: response }); 136 | return response.data[0].b64_json!; 137 | }; 138 | -------------------------------------------------------------------------------- /src/utils/csvUtils.ts: -------------------------------------------------------------------------------- 1 | export function parseCsv(csvString: string): string[][] { 2 | // Split the CSV string by line breaks to get an array of rows 3 | const rows = csvString.split("\n"); 4 | 5 | // Map each row to an array of values (split by comma) 6 | return rows.map((row) => { 7 | // Handling potential quotes in CSV 8 | return (row.match(/(".*?"|[^",]+)(?=\s*,|\s*$)/g) || []).map( 9 | (str: string) => str.slice(1, -1) 10 | ); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/websiteContentUtils.ts: -------------------------------------------------------------------------------- 1 | export const getWebsiteContent = async (url: string) => { 2 | // // console.log({ getWebsiteContent2: true }) 3 | // const content = await fetch(url, { 4 | // // mode: "no-cors", 5 | // }); 6 | // console.log({ content, body: content.body }); 7 | // return {}; 8 | // const getMDForTagName = (tagName: string) => { 9 | // if (tagName === "h1") { 10 | // return "#"; 11 | // } else if (tagName === "h2") { 12 | // return "##"; 13 | // } else if (tagName === "h3") { 14 | // return "###"; 15 | // } else if (tagName === "h4") { 16 | // return "####"; 17 | // } else if (tagName === "h5") { 18 | // return "#####"; 19 | // } else if (tagName === "h6") { 20 | // return "######"; 21 | // } 22 | // }; 23 | // // let count = 0; 24 | // let textContent = ""; 25 | // // const selectors = []; 26 | // // function fullPath(el) { 27 | // // var names = []; 28 | // // while (el.parentNode) { 29 | // // if (el.id) { 30 | // // names.unshift("#" + el.id); 31 | // // break; 32 | // // } else { 33 | // // if (el == el.ownerDocument.documentElement) 34 | // // names.unshift(el.tagName); 35 | // // else { 36 | // // for ( 37 | // // var c = 1, e = el; 38 | // // e.previousElementSibling; 39 | // // e = e.previousElementSibling, c++ 40 | // // ); 41 | // // names.unshift(el.tagName + ":nth-child(" + c + ")"); 42 | // // } 43 | // // el = el.parentNode; 44 | // // } 45 | // // } 46 | // // return names.join(" > "); 47 | // // } 48 | // // Function to traverse all elements in the DOM 49 | // function traverseDOM(element: Element): void { 50 | // // Process the current element 51 | // // console.log(element.tagName); 52 | // const includedTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6"]; 53 | // // const excludedTags = ["script", "button"] 54 | // if ( 55 | // includedTags.includes(element.tagName.toLowerCase()) 56 | // // && 57 | // // element.textContent.split(" ").length > 5 58 | // ) { 59 | // const text = element.textContent 60 | // ?.replace(/\n/g, " ") 61 | // .replace(/\\n/g, "") 62 | // .replace(/\t/g, "") 63 | // .replace(/\\t/g, "") 64 | // .trim(); 65 | // // console.log({ text, tagName: element.tagName }) 66 | // // * Example: 1. ### Title 67 | // textContent += 68 | // "\n\n" + 69 | // // `${count}.` + 70 | // (element.tagName.toLowerCase() !== "p" 71 | // ? getMDForTagName(element.tagName.toLowerCase()) + " " 72 | // : "") + 73 | // text; 74 | // // count++; 75 | // // const path = fullPath(element); 76 | // // selectors.push(path); 77 | // // document.querySelector(path).scrollIntoView() 78 | // } 79 | // // Recursively traverse child elements 80 | // Array.from(element.children).forEach((child) => { 81 | // traverseDOM(child); 82 | // }); 83 | // } 84 | // // Example usage 85 | // // document.addEventListener('DOMContentLoaded', () => { 86 | // traverseDOM(document.documentElement); 87 | // // }); 88 | // console.log({ 89 | // // selectors, 90 | // textContent, 91 | // }); 92 | // return { 93 | // textContent, 94 | // // selectors, 95 | // }; 96 | }; 97 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .augmented-canvas-modal-container { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 16px; 5 | justify-content: end; 6 | align-items: end; 7 | } 8 | 9 | .augmented-canvas-modal-textarea { 10 | box-sizing: border-box; 11 | min-height: 70px; 12 | flex-grow: 1; 13 | width: 100%; 14 | } 15 | 16 | .augmented-canvas-modal-input { 17 | box-sizing: border-box; 18 | flex-grow: 1; 19 | width: 100%; 20 | padding: 12px; 21 | } 22 | 23 | /* */ 24 | 25 | .augmented-canvas-setting-item { 26 | display: flex; 27 | flex-direction: column; 28 | gap: 16px; 29 | } 30 | 31 | .augmented-canvas-setting-item > .setting-item-control { 32 | width: 100%; 33 | display: flex; 34 | justify-content: start; 35 | align-items: start; 36 | gap: 16px; 37 | } 38 | 39 | .augmented-canvas-settings-prompt { 40 | flex-grow: 1; 41 | width: 300px; 42 | } 43 | 44 | .augmented-canvas-list-container { 45 | } 46 | 47 | .augmented-canvas-list-element { 48 | width: 100%; 49 | display: flex; 50 | justify-content: start; 51 | align-items: start; 52 | gap: 16px; 53 | padding: 0.75em 0; 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "1.1.0" 3 | } --------------------------------------------------------------------------------