├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------