notes-to-strapi-export-article-ai 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +10 -0
- package/.eslintignore +3 -0
- package/.eslintrc +24 -0
- package/.github/workflows/deploy.yml +58 -0
- package/.github/workflows/publish.yml +30 -0
- package/.prettierrc +19 -0
- package/LICENSE +7 -0
- package/README.md +179 -0
- package/esbuild.config.mjs +48 -0
- package/img.png +0 -0
- package/img_1.png +0 -0
- package/img_2.png +0 -0
- package/img_3.png +0 -0
- package/img_4.png +0 -0
- package/img_5.png +0 -0
- package/img_6.png +0 -0
- package/img_7.png +0 -0
- package/img_8.png +0 -0
- package/img_9.png +0 -0
- package/main.ts +687 -0
- package/manifest.json +11 -0
- package/package.json +27 -0
- package/prettier.config.js +19 -0
- package/renovate.json +18 -0
- package/styles.css +8 -0
- package/tsconfig.json +24 -0
- package/version-bump.mjs +14 -0
- package/versions.json +3 -0
package/.editorconfig
ADDED
package/.eslintignore
ADDED
package/.eslintrc
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
"@typescript-eslint/no-explicit-any": "off"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Auto Deploy, install dependencies, increment version and push tag
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
types: [ closed ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
if: github.event.pull_request.merged == true
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
token: ${{ secrets.GIT_TOKEN }}
|
|
18
|
+
ref: 'main'
|
|
19
|
+
|
|
20
|
+
- name: Configure Git
|
|
21
|
+
run: |
|
|
22
|
+
git config --local user.email "cinquin.andy@gmail.com"
|
|
23
|
+
git config --local user.name "Cinquin Andy"
|
|
24
|
+
|
|
25
|
+
- name: Use Node.js
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: '20'
|
|
29
|
+
registry-url: 'https://registry.npmjs.org'
|
|
30
|
+
|
|
31
|
+
- name: Install Dependencies
|
|
32
|
+
run: npm ci
|
|
33
|
+
|
|
34
|
+
- name: Increment Package Version and Update Tag
|
|
35
|
+
id: version-bump
|
|
36
|
+
run: |
|
|
37
|
+
git fetch --tags
|
|
38
|
+
current_version=$(npm pkg get version | sed 's/"//g')
|
|
39
|
+
echo "Current version: $current_version"
|
|
40
|
+
|
|
41
|
+
new_version=$(npx semver -i patch $current_version)
|
|
42
|
+
echo "New version: $new_version"
|
|
43
|
+
|
|
44
|
+
while git rev-parse v$new_version >/dev/null 2>&1; do
|
|
45
|
+
new_version=$(npx semver -i patch $new_version)
|
|
46
|
+
echo "Tag v$new_version already exists. Trying next version..."
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
npm version $new_version --no-git-tag-version
|
|
50
|
+
echo "::set-output name=VERSION::$new_version"
|
|
51
|
+
|
|
52
|
+
- name: Commit and Push Version Update
|
|
53
|
+
run: |
|
|
54
|
+
git add package.json
|
|
55
|
+
git commit -m "chore(release): ${{ steps.version-bump.outputs.VERSION }}"
|
|
56
|
+
git tag v${{ steps.version-bump.outputs.VERSION }}
|
|
57
|
+
git push origin main --follow-tags
|
|
58
|
+
git push origin refs/tags/v${{ steps.version-bump.outputs.VERSION }}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to npm when a new tag is pushed
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
token: ${{ secrets.GIT_TOKEN }}
|
|
17
|
+
|
|
18
|
+
- name: Use Node.js
|
|
19
|
+
uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '20'
|
|
22
|
+
registry-url: 'https://registry.npmjs.org'
|
|
23
|
+
|
|
24
|
+
- name: Install Dependencies
|
|
25
|
+
run: npm ci
|
|
26
|
+
|
|
27
|
+
- name: Publish to npm
|
|
28
|
+
run: npm publish
|
|
29
|
+
env:
|
|
30
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/.prettierrc
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"singleQuote": true,
|
|
3
|
+
"semi": false,
|
|
4
|
+
"trailingComma": "es5",
|
|
5
|
+
"tabWidth": 2,
|
|
6
|
+
"useTabs": true,
|
|
7
|
+
"printWidth": 80,
|
|
8
|
+
"arrowParens": "avoid",
|
|
9
|
+
"bracketSpacing": true,
|
|
10
|
+
"jsxSingleQuote": false,
|
|
11
|
+
"proseWrap": "preserve",
|
|
12
|
+
"quoteProps": "as-needed",
|
|
13
|
+
"requirePragma": false,
|
|
14
|
+
"insertPragma": false,
|
|
15
|
+
"endOfLine": "lf",
|
|
16
|
+
"htmlWhitespaceSensitivity": "css",
|
|
17
|
+
"vueIndentScriptAndStyle": false,
|
|
18
|
+
"embeddedLanguageFormatting": "auto"
|
|
19
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2024> <CINQUIN Andy>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# 🚀 Strapi Exporter: Supercharge Your Obsidian-to-Strapi Workflow
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CinquinAndy/notes-to-strapi-export-article-ai/releases)
|
|
4
|
+
[](https://github.com/CinquinAndy/notes-to-strapi-export-article-ai/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/sponsors/CinquinAndy)
|
|
6
|
+
|
|
7
|
+
Strapi Exporter is a game-changing Obsidian plugin that streamlines your content creation process by seamlessly exporting your notes to Strapi CMS. With its AI-powered image handling and SEO optimization features, you can take your content to the next level with just a few clicks.
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- 🖼️ Automatically upload images from your notes to Strapi
|
|
12
|
+
- 🎨 Generate SEO-friendly alt text and captions for images using AI
|
|
13
|
+
- 📝 Create SEO-optimized article content based on your notes
|
|
14
|
+
- 🔧 Customize the JSON template for the article fields in Strapi
|
|
15
|
+
- ⚙️ Easy configuration for Strapi API URL, token, and content attribute name
|
|
16
|
+
|
|
17
|
+
## 🛠️ Installation
|
|
18
|
+
|
|
19
|
+
### For users:
|
|
20
|
+
|
|
21
|
+
1. Download the latest release from the [releases page](https://github.com/CinquinAndy/notes-to-strapi-export-article-ai/releases/tag/)
|
|
22
|
+
2. Download the `main.js` file & `manifest.json` from the latest release.
|
|
23
|
+
3. Create a folder named `notes-to-strapi-export-article-ai` in your Obsidian plugins folder (usually located at `<vault>/.obsidian/plugins/`).
|
|
24
|
+
4. Move the `main.js` file & `manifest.json` to your Obsidian plugins folder (usually located at `<vault>/.obsidian/plugins/`).
|
|
25
|
+
5. Restart Obsidian
|
|
26
|
+
6. Enable the plugin in Obsidian's settings under "Community plugins".
|
|
27
|
+
7. Configure the necessary settings (see the Configuration section below).
|
|
28
|
+
8. Enjoy!
|
|
29
|
+
|
|
30
|
+
### For developers:
|
|
31
|
+
|
|
32
|
+
To install Strapi Exporter, follow these steps (coming soon to the Obsidian plugin marketplace):
|
|
33
|
+
|
|
34
|
+
1. Clone this repository into your Obsidian plugins folder (usually located at `<vault>/.obsidian/plugins/`).
|
|
35
|
+
2. Run `npm install` to install the dependencies
|
|
36
|
+
3. Run `npm run build` to build the plugin
|
|
37
|
+
4. Restart Obsidian
|
|
38
|
+
5. Enable the plugin in Obsidian's settings under "Community plugins".
|
|
39
|
+
6. Configure the necessary settings (see the Configuration section below).
|
|
40
|
+
|
|
41
|
+
## ⚙️ Configuration
|
|
42
|
+
|
|
43
|
+
To get started with Strapi Exporter, you'll need to configure the following settings:
|
|
44
|
+
|
|
45
|
+
- **Strapi URL**: The URL of your Strapi instance (e.g., `https://your-strapi-url`).
|
|
46
|
+

|
|
47
|
+
- **Strapi API Token**: Your Strapi API token for authentication. You can create an API token in your Strapi admin panel under "Settings" > "API Tokens".
|
|
48
|
+

|
|
49
|
+

|
|
50
|
+
- You need, at least, to have the following permissions:
|
|
51
|
+
- article: create
|
|
52
|
+
- upload: create
|
|
53
|
+
- (you can also add full permissions, but it's not really recommended for security reasons)
|
|
54
|
+

|
|
55
|
+
- (the token in the screenshot is not valid, don't try to use it 😌)
|
|
56
|
+
- **OpenAI API Key**: Your OpenAI API key for using GPT-3 to generate SEO-friendly content. You can get your API key from the [OpenAI website](https://platform.openai.com/account/api-keys).
|
|
57
|
+

|
|
58
|
+
- this key is needed to use the GPT-3 API, which is used to generate the content of the article
|
|
59
|
+
- (it need to access to "Model capabilities" with "write" permission)
|
|
60
|
+

|
|
61
|
+
- (or with the "all" permission)
|
|
62
|
+
- **JSON Template**: The JSON template for the article fields in Strapi. Customize this according to your Strapi content type structure. You can find the JSON template in your Strapi API documentation (Swagger).
|
|
63
|
+

|
|
64
|
+
- to get the JSON template, you can go to the documentation of your Strapi API, and copy the JSON template of the article creation
|
|
65
|
+
- it should look like this: ``https://{api_url}/documentation/v1.0.0``
|
|
66
|
+
- then, go to the article creation, and copy the JSON template
|
|
67
|
+

|
|
68
|
+

|
|
69
|
+
- it should look like this for example:
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"data": {
|
|
73
|
+
"title": "string",
|
|
74
|
+
"seo_title": "string",
|
|
75
|
+
"seo_description": "string",
|
|
76
|
+
"slug": "string",
|
|
77
|
+
"excerpt": "string",
|
|
78
|
+
"links": [
|
|
79
|
+
{
|
|
80
|
+
"id": "number",
|
|
81
|
+
"label": "string",
|
|
82
|
+
"url": "string"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"subtitle": "string",
|
|
86
|
+
"type": "string",
|
|
87
|
+
"rank": "number",
|
|
88
|
+
"tags": [
|
|
89
|
+
{
|
|
90
|
+
"id": "number",
|
|
91
|
+
"name": "string"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"locale": "string"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
- then, you can copy this JSON template in the settings of the plugin
|
|
99
|
+
- and copy that, to describe each field in the other JSON description setting
|
|
100
|
+
- **JSON Template Description**: A description for each field in the JSON template to help GPT-3 understand the structure. Follow the same schema as the JSON template to provide descriptions for each field.
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"data": {
|
|
104
|
+
"title": "<Title of the item, as a short string>",
|
|
105
|
+
"seo_title": "<SEO optimized title, as a short string>",
|
|
106
|
+
"seo_description": "<SEO optimized description, as a short string>",
|
|
107
|
+
"slug": "<URL-friendly string derived from the title>",
|
|
108
|
+
"excerpt": "<A short preview or snippet from the content>",
|
|
109
|
+
"links": [
|
|
110
|
+
{
|
|
111
|
+
"id": "<Unique identifier for the link, as a number>",
|
|
112
|
+
"label": "<Display text for the link, as a short string>",
|
|
113
|
+
"url": "<URL the link points to, as a string>"
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"subtitle": "<Subtitle or secondary title, as a short string>",
|
|
117
|
+
"type": "<Category or type of the item, as a short string>",
|
|
118
|
+
"rank": "<Numerical ranking or order priority, as a number>",
|
|
119
|
+
"tags": [
|
|
120
|
+
{
|
|
121
|
+
"id": "<Unique identifier for the tag, as a number>",
|
|
122
|
+
"name": "<Name of the tag, as a short string>"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"locale": "<Locale or language code, as a short string>"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
```
|
|
130
|
+
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
|
131
|
+
⚠️
|
|
132
|
+
⚠️ **Important:** Remove the `content` (or the attribute name that correspond to the main content of the article)
|
|
133
|
+
⚠️ field from the JSON template and specify it separately in the "Strapi Content Attribute Name" setting.
|
|
134
|
+
⚠️ ( i do that to avoid changes on the main content by chat gpt )
|
|
135
|
+
⚠️
|
|
136
|
+
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
|
137
|
+
```
|
|
138
|
+
- **Strapi Article Create URL**: The URL to create articles in Strapi (e.g., `https://your-strapi-url/api/articles`).
|
|
139
|
+
- to get the URL, you can go to the documentation of your Strapi API, and copy the URL of the article creation
|
|
140
|
+
- it should look like this: ``https://{api_url}/documentation/v1.0.0``
|
|
141
|
+
- then, go to the article creation, and copy the URL
|
|
142
|
+
- **Strapi Content Attribute Name**: The attribute name for the content field in Strapi (e.g., `content`).
|
|
143
|
+
- to get the attribute name, you can go to the documentation of your Strapi API, and copy the attribute name of the article creation, (it need to be the name of the attribute that contain the main content of the article, for me it's "content", but it can be different for you)
|
|
144
|
+
- **Additional Prompt** (optional): Additional prompt to provide context for GPT-3 when generating content. You can use this field to specify additional information or instructions for the AI model. Like your langage, the tone of the article, etc.
|
|
145
|
+
|
|
146
|
+
## 🚀 Usage
|
|
147
|
+
|
|
148
|
+
1. Open a Markdown file in Obsidian.
|
|
149
|
+
2. Click on the plugin's ribbon icon to start the magic.
|
|
150
|
+

|
|
151
|
+
3. Sit back and relax while Strapi Exporter does the heavy lifting:
|
|
152
|
+
- 🖼️ Extracting and uploading images to Strapi
|
|
153
|
+
- 🎨 Generating SEO-friendly alt text and captions for images
|
|
154
|
+
- 📝 Creating SEO-optimized article content based on your notes
|
|
155
|
+
- 🌐 Publishing the article to Strapi with the generated content and images
|
|
156
|
+
4. Enjoy your freshly exported article in Strapi!
|
|
157
|
+
|
|
158
|
+
## 🤝 Contributing
|
|
159
|
+
|
|
160
|
+
We welcome contributions from the community! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request. Let's make Strapi Exporter even better together!
|
|
161
|
+
|
|
162
|
+
## 📜 License
|
|
163
|
+
|
|
164
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
🌟 Elevate your content workflow with Strapi Exporter and unleash the full potential of your Obsidian notes! 🌟
|
|
169
|
+
|
|
170
|
+
### Roadmap
|
|
171
|
+
|
|
172
|
+
- [ ] Make it available as a plugin in Obsidian
|
|
173
|
+
- [ ] Add tests
|
|
174
|
+
- [ ] Renovate
|
|
175
|
+
- [ ] ajouter l'étape de création du plugin (création du folder)
|
|
176
|
+
- [ ] ajouter l'étape de redémarrage d'obsidian
|
|
177
|
+
- [ ] ajouter l'étape de configuration du plugin
|
|
178
|
+
- [ ] ajouter l'étape de configuration du token d'access de strapi (accès etc)
|
|
179
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import esbuild from "esbuild";
|
|
2
|
+
import process from "process";
|
|
3
|
+
import builtins from "builtin-modules";
|
|
4
|
+
|
|
5
|
+
const banner =
|
|
6
|
+
`/*
|
|
7
|
+
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
8
|
+
if you want to view the source, please visit the github repository of this plugin
|
|
9
|
+
*/
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
const prod = (process.argv[2] === "production");
|
|
13
|
+
|
|
14
|
+
const context = await esbuild.context({
|
|
15
|
+
banner: {
|
|
16
|
+
js: banner,
|
|
17
|
+
},
|
|
18
|
+
entryPoints: ["main.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
|
+
format: "cjs",
|
|
36
|
+
target: "es2018",
|
|
37
|
+
logLevel: "info",
|
|
38
|
+
sourcemap: prod ? false : "inline",
|
|
39
|
+
treeShaking: true,
|
|
40
|
+
outfile: "main.js",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (prod) {
|
|
44
|
+
await context.rebuild();
|
|
45
|
+
process.exit(0);
|
|
46
|
+
} else {
|
|
47
|
+
await context.watch();
|
|
48
|
+
}
|
package/img.png
ADDED
|
Binary file
|
package/img_1.png
ADDED
|
Binary file
|
package/img_2.png
ADDED
|
Binary file
|
package/img_3.png
ADDED
|
Binary file
|
package/img_4.png
ADDED
|
Binary file
|
package/img_5.png
ADDED
|
Binary file
|
package/img_6.png
ADDED
|
Binary file
|
package/img_7.png
ADDED
|
Binary file
|
package/img_8.png
ADDED
|
Binary file
|
package/img_9.png
ADDED
|
Binary file
|
package/main.ts
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import OpenAI from 'openai'
|
|
2
|
+
import {
|
|
3
|
+
App,
|
|
4
|
+
MarkdownView,
|
|
5
|
+
Notice,
|
|
6
|
+
Plugin,
|
|
7
|
+
PluginSettingTab,
|
|
8
|
+
Setting,
|
|
9
|
+
TFile,
|
|
10
|
+
} from 'obsidian'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The settings for the plugin
|
|
14
|
+
*/
|
|
15
|
+
interface MyPluginSettings {
|
|
16
|
+
strapiUrl: string
|
|
17
|
+
strapiApiToken: string
|
|
18
|
+
openaiApiKey: string
|
|
19
|
+
jsonTemplate: string
|
|
20
|
+
jsonTemplateDescription: string
|
|
21
|
+
strapiArticleCreateUrl: string
|
|
22
|
+
strapiContentAttributeName: string
|
|
23
|
+
additionalPrompt: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The default settings for the plugin
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
30
|
+
strapiUrl: '',
|
|
31
|
+
strapiApiToken: '',
|
|
32
|
+
openaiApiKey: '',
|
|
33
|
+
jsonTemplate: `{
|
|
34
|
+
"data": {
|
|
35
|
+
"title": "string",
|
|
36
|
+
"seo_title": "string",
|
|
37
|
+
"seo_description": "string",
|
|
38
|
+
"slug": "string",
|
|
39
|
+
"excerpt": "string",
|
|
40
|
+
"links": [
|
|
41
|
+
{
|
|
42
|
+
"id": "number",
|
|
43
|
+
"label": "string",
|
|
44
|
+
"url": "string"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"subtitle": "string",
|
|
48
|
+
"type": "string",
|
|
49
|
+
"rank": "number",
|
|
50
|
+
"tags": [
|
|
51
|
+
{
|
|
52
|
+
"id": "number",
|
|
53
|
+
"name": "string"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"locale": "string"
|
|
57
|
+
}
|
|
58
|
+
}`,
|
|
59
|
+
jsonTemplateDescription: `{
|
|
60
|
+
"data": {
|
|
61
|
+
"title": "Title of the item, as a short string",
|
|
62
|
+
"seo_title": "SEO optimized title, as a short string",
|
|
63
|
+
"seo_description": "SEO optimized description, as a short string",
|
|
64
|
+
"slug": "URL-friendly string derived from the title",
|
|
65
|
+
"excerpt": "A short preview or snippet from the content",
|
|
66
|
+
"links": "Array of related links with ID, label, and URL",
|
|
67
|
+
"subtitle": "Subtitle or secondary title, as a short string",
|
|
68
|
+
"type": "Category or type of the item, as a short string",
|
|
69
|
+
"rank": "Numerical ranking or order priority, as a number",
|
|
70
|
+
"tags": "Array of associated tags with ID and name",
|
|
71
|
+
"locale": "Locale or language code, as a short string"
|
|
72
|
+
}
|
|
73
|
+
}`,
|
|
74
|
+
strapiArticleCreateUrl: '',
|
|
75
|
+
strapiContentAttributeName: '',
|
|
76
|
+
additionalPrompt: '',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The main plugin class
|
|
81
|
+
*/
|
|
82
|
+
export default class MyPlugin extends Plugin {
|
|
83
|
+
settings: MyPluginSettings
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The main entry point for the plugin
|
|
87
|
+
*/
|
|
88
|
+
async onload() {
|
|
89
|
+
await this.loadSettings()
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add a ribbon icon to the Markdown view (the little icon on the left side bar)
|
|
93
|
+
*/
|
|
94
|
+
const ribbonIconEl = this.addRibbonIcon(
|
|
95
|
+
'upload',
|
|
96
|
+
'Upload images to Strapi and update links in Markdown content, then generate article content using OpenAI',
|
|
97
|
+
async (evt: MouseEvent) => {
|
|
98
|
+
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView)
|
|
99
|
+
if (!activeView) {
|
|
100
|
+
new Notice('No active Markdown view')
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** ****************************************************************************
|
|
105
|
+
* Check if all the settings are configured
|
|
106
|
+
* *****************************************************************************
|
|
107
|
+
*/
|
|
108
|
+
if (!this.settings.strapiUrl || !this.settings.strapiApiToken) {
|
|
109
|
+
new Notice(
|
|
110
|
+
'Please configure Strapi URL and API token in the plugin settings'
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!this.settings.openaiApiKey) {
|
|
116
|
+
new Notice('Please configure OpenAI API key in the plugin settings')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!this.settings.jsonTemplate) {
|
|
121
|
+
new Notice('Please configure JSON template in the plugin settings')
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!this.settings.jsonTemplateDescription) {
|
|
126
|
+
new Notice(
|
|
127
|
+
'Please configure JSON template description in the plugin settings'
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!this.settings.strapiArticleCreateUrl) {
|
|
133
|
+
new Notice(
|
|
134
|
+
'Please configure Strapi article create URL in the plugin settings'
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!this.settings.strapiContentAttributeName) {
|
|
140
|
+
new Notice(
|
|
141
|
+
'Please configure Strapi content attribute name in the plugin settings'
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** ****************************************************************************
|
|
147
|
+
* Process the Markdown content
|
|
148
|
+
* *****************************************************************************
|
|
149
|
+
*/
|
|
150
|
+
new Notice('All settings are ok, processing Markdown content...')
|
|
151
|
+
const file = activeView.file
|
|
152
|
+
let content = ''
|
|
153
|
+
if (!file) {
|
|
154
|
+
new Notice('No file found in active view...')
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Read the content of the file
|
|
159
|
+
*/
|
|
160
|
+
content = await this.app.vault.read(file)
|
|
161
|
+
|
|
162
|
+
// check if the content has any images to process
|
|
163
|
+
const flag = this.hasUnexportedImages(content)
|
|
164
|
+
/**
|
|
165
|
+
* Initialize the OpenAI API
|
|
166
|
+
*/
|
|
167
|
+
const openai = new OpenAI({
|
|
168
|
+
apiKey: this.settings.openaiApiKey,
|
|
169
|
+
dangerouslyAllowBrowser: true,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Process the images in the content, upload them to Strapi, and update the links,
|
|
174
|
+
* only if there are images in the content
|
|
175
|
+
* that are not already uploaded to Strapi
|
|
176
|
+
*/
|
|
177
|
+
if (flag) {
|
|
178
|
+
/**
|
|
179
|
+
* Extract the image paths from the content
|
|
180
|
+
*/
|
|
181
|
+
const imagePaths = this.extractImagePaths(content)
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get the image blobs from the image paths
|
|
185
|
+
*/
|
|
186
|
+
const imageBlobs = await this.getImageBlobs(imagePaths)
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the image descriptions using the OpenAI API
|
|
190
|
+
*/
|
|
191
|
+
new Notice('Getting image descriptions...')
|
|
192
|
+
const imageDescriptions = await Promise.all(
|
|
193
|
+
imageBlobs.map(async imageBlob => {
|
|
194
|
+
const description = await this.getImageDescription(
|
|
195
|
+
imageBlob.blob,
|
|
196
|
+
openai
|
|
197
|
+
)
|
|
198
|
+
return {
|
|
199
|
+
blob: imageBlob.blob,
|
|
200
|
+
name: imageBlob.name,
|
|
201
|
+
path: imageBlob.path,
|
|
202
|
+
description,
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Upload the images to Strapi
|
|
209
|
+
*/
|
|
210
|
+
new Notice('Uploading images to Strapi...')
|
|
211
|
+
const uploadedImages =
|
|
212
|
+
await this.uploadImagesToStrapi(imageDescriptions)
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Replace the image paths in the content with the uploaded image URLs
|
|
216
|
+
*/
|
|
217
|
+
new Notice('Replacing image paths...')
|
|
218
|
+
content = this.replaceImagePaths(content, uploadedImages)
|
|
219
|
+
await this.app.vault.modify(file, content)
|
|
220
|
+
new Notice('Images uploaded and links updated successfully!')
|
|
221
|
+
} else {
|
|
222
|
+
new Notice(
|
|
223
|
+
'No local images found in the content... Skip the image processing...'
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Generate article content using OpenAI
|
|
229
|
+
*/
|
|
230
|
+
new Notice('Generating article content...')
|
|
231
|
+
/**
|
|
232
|
+
* Parse the JSON template and description
|
|
233
|
+
*/
|
|
234
|
+
const jsonTemplate = JSON.parse(this.settings.jsonTemplate)
|
|
235
|
+
const jsonTemplateDescription = JSON.parse(
|
|
236
|
+
this.settings.jsonTemplateDescription
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* If the content is not present, get it from the active view
|
|
241
|
+
*/
|
|
242
|
+
content = await this.app.vault.read(file)
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Prompt for generating the article content
|
|
246
|
+
*/
|
|
247
|
+
const articlePrompt = `You are an SEO expert. Generate an article based on the following template and field descriptions:
|
|
248
|
+
|
|
249
|
+
Template:
|
|
250
|
+
${JSON.stringify(jsonTemplate, null, 2)}
|
|
251
|
+
|
|
252
|
+
Field Descriptions:
|
|
253
|
+
${JSON.stringify(jsonTemplateDescription, null, 2)}
|
|
254
|
+
|
|
255
|
+
The main content of the article should be based on the following text and all the keywords around the domain of the text:
|
|
256
|
+
----- CONTENT -----
|
|
257
|
+
${content.substring(0, 500)}
|
|
258
|
+
----- END CONTENT -----
|
|
259
|
+
|
|
260
|
+
Please provide the generated article content as a JSON object following the given template structure.
|
|
261
|
+
|
|
262
|
+
${this.settings.additionalPrompt ? `Additional Prompt: ${this.settings.additionalPrompt}` : ''}`
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Generate the article content using OpenAI
|
|
266
|
+
*/
|
|
267
|
+
const completion = await openai.chat.completions.create({
|
|
268
|
+
model: 'gpt-3.5-turbo-0125',
|
|
269
|
+
messages: [
|
|
270
|
+
{
|
|
271
|
+
role: 'user',
|
|
272
|
+
content: articlePrompt,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
max_tokens: 2000,
|
|
276
|
+
n: 1,
|
|
277
|
+
stop: null,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Parse the generated article content
|
|
282
|
+
*/
|
|
283
|
+
let articleContent = JSON.parse(
|
|
284
|
+
completion.choices[0].message.content ?? '{}'
|
|
285
|
+
)
|
|
286
|
+
/**
|
|
287
|
+
* Add the content to the article content
|
|
288
|
+
*/
|
|
289
|
+
articleContent = {
|
|
290
|
+
data: {
|
|
291
|
+
...articleContent.data,
|
|
292
|
+
[this.settings.strapiContentAttributeName]: content,
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
new Notice('Article content generated successfully!')
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(this.settings.strapiArticleCreateUrl, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: {
|
|
301
|
+
'Content-Type': 'application/json',
|
|
302
|
+
Authorization: `Bearer ${this.settings.strapiApiToken}`,
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify(articleContent),
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (response.ok) {
|
|
308
|
+
new Notice('Article created successfully in Strapi!')
|
|
309
|
+
} else {
|
|
310
|
+
new Notice('Failed to create article in Strapi.')
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
new Notice('Error creating article in Strapi.')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
new Notice(
|
|
317
|
+
'Check your API content now, the article is created & uploaded ! 🎉'
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
ribbonIconEl.addClass('my-plugin-ribbon-class')
|
|
322
|
+
|
|
323
|
+
this.addSettingTab(new MyExportSettingTab(this.app, this))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
onunload() {}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Load the settings for the plugin
|
|
330
|
+
*/
|
|
331
|
+
async loadSettings() {
|
|
332
|
+
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Save the settings for the plugin
|
|
337
|
+
*/
|
|
338
|
+
async saveSettings() {
|
|
339
|
+
await this.saveData(this.settings)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Extract the image paths from the content
|
|
344
|
+
* @param content
|
|
345
|
+
*/
|
|
346
|
+
extractImagePaths(content: string): string[] {
|
|
347
|
+
/**
|
|
348
|
+
* Extract the image paths from the content
|
|
349
|
+
*/
|
|
350
|
+
const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
|
|
351
|
+
const imagePaths: string[] = []
|
|
352
|
+
let match
|
|
353
|
+
|
|
354
|
+
while ((match = imageRegex.exec(content)) !== null) {
|
|
355
|
+
imagePaths.push(match[1])
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return imagePaths
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Check if the content has any unexported images
|
|
363
|
+
* @param content
|
|
364
|
+
*/
|
|
365
|
+
hasUnexportedImages(content: string): boolean {
|
|
366
|
+
const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
|
|
367
|
+
return imageRegex.test(content)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get the image blobs from the image paths
|
|
372
|
+
* @param imagePaths
|
|
373
|
+
*/
|
|
374
|
+
async getImageBlobs(
|
|
375
|
+
imagePaths: string[]
|
|
376
|
+
): Promise<{ path: string; blob: Blob; name: string }[]> {
|
|
377
|
+
// get all the files in the vault
|
|
378
|
+
const files = this.app.vault.getAllLoadedFiles()
|
|
379
|
+
// get the image files name from the vault
|
|
380
|
+
const fileNames = files.map(file => file.name)
|
|
381
|
+
// filter the image files, and get all the images files paths
|
|
382
|
+
const imageFiles = imagePaths.filter(path => fileNames.includes(path))
|
|
383
|
+
// get the image blobs, find it, and return the blob
|
|
384
|
+
return await Promise.all(
|
|
385
|
+
imageFiles.map(async path => {
|
|
386
|
+
const file = files.find(file => file.name === path)
|
|
387
|
+
if (file instanceof TFile) {
|
|
388
|
+
const blob = await this.app.vault.readBinary(file)
|
|
389
|
+
return {
|
|
390
|
+
name: path,
|
|
391
|
+
blob: new Blob([blob], { type: 'image/png' }),
|
|
392
|
+
path: file.path,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
name: '',
|
|
397
|
+
blob: new Blob(),
|
|
398
|
+
path: '',
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Upload the images to Strapi
|
|
406
|
+
* @param imageBlobs
|
|
407
|
+
*/
|
|
408
|
+
async uploadImagesToStrapi(
|
|
409
|
+
imageBlobs: {
|
|
410
|
+
path: string
|
|
411
|
+
blob: Blob
|
|
412
|
+
name: string
|
|
413
|
+
description: {
|
|
414
|
+
name: string
|
|
415
|
+
alternativeText: string
|
|
416
|
+
caption: string
|
|
417
|
+
}
|
|
418
|
+
}[]
|
|
419
|
+
): Promise<{ [key: string]: { url: string; data: any } }> {
|
|
420
|
+
// upload the images to Strapi
|
|
421
|
+
const uploadedImages: {
|
|
422
|
+
[key: string]: { url: string; data: any }
|
|
423
|
+
} = {}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Upload the images to Strapi
|
|
427
|
+
*/
|
|
428
|
+
for (const imageBlob of imageBlobs) {
|
|
429
|
+
const formData = new FormData()
|
|
430
|
+
/**
|
|
431
|
+
* Append the image blob and the image description to the form data
|
|
432
|
+
*/
|
|
433
|
+
formData.append('files', imageBlob.blob, imageBlob.name)
|
|
434
|
+
formData.append(
|
|
435
|
+
'fileInfo',
|
|
436
|
+
JSON.stringify({
|
|
437
|
+
name: imageBlob.description.name,
|
|
438
|
+
alternativeText: imageBlob.description.alternativeText,
|
|
439
|
+
caption: imageBlob.description.caption,
|
|
440
|
+
})
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
// upload the image to Strapi
|
|
444
|
+
try {
|
|
445
|
+
const response = await fetch(`${this.settings.strapiUrl}/api/upload`, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers: {
|
|
448
|
+
Authorization: `Bearer ${this.settings.strapiApiToken}`,
|
|
449
|
+
},
|
|
450
|
+
body: formData,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* If the response is ok, add the uploaded image to the uploaded images object
|
|
455
|
+
*/
|
|
456
|
+
if (response.ok) {
|
|
457
|
+
const data = await response.json()
|
|
458
|
+
uploadedImages[imageBlob.name] = {
|
|
459
|
+
url: data[0].url,
|
|
460
|
+
data: data[0],
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
new Notice(`Failed to upload image: ${imageBlob.name}`)
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
new Notice(`Error uploading image: ${imageBlob.name}`)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return uploadedImages
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Replace the image paths in the content with the uploaded image URLs
|
|
475
|
+
* @param content
|
|
476
|
+
* @param uploadedImages
|
|
477
|
+
*/
|
|
478
|
+
replaceImagePaths(
|
|
479
|
+
content: string,
|
|
480
|
+
uploadedImages: { [key: string]: { url: string; data: any } }
|
|
481
|
+
): string {
|
|
482
|
+
/**
|
|
483
|
+
* Replace the image paths in the content with the uploaded image URLs
|
|
484
|
+
*/
|
|
485
|
+
for (const [localPath, imageData] of Object.entries(uploadedImages)) {
|
|
486
|
+
const markdownImageRegex = new RegExp(`!\\[\\[${localPath}\\]\\]`, 'g')
|
|
487
|
+
content = content.replace(
|
|
488
|
+
markdownImageRegex,
|
|
489
|
+
``
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
return content
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get the description of the image using OpenAI
|
|
497
|
+
* @param imageBlob
|
|
498
|
+
* @param openai
|
|
499
|
+
*/
|
|
500
|
+
async getImageDescription(imageBlob: Blob, openai: OpenAI) {
|
|
501
|
+
// get the image description using the OpenAI API ( using gpt 4 vision preview model )
|
|
502
|
+
const response = await openai.chat.completions.create({
|
|
503
|
+
model: 'gpt-4-vision-preview',
|
|
504
|
+
messages: [
|
|
505
|
+
{
|
|
506
|
+
role: 'user',
|
|
507
|
+
// @ts-ignore
|
|
508
|
+
content: [
|
|
509
|
+
{
|
|
510
|
+
type: 'text',
|
|
511
|
+
text: `What's in this image? make it simple, i just want the context and an idea(think about alt text)`,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
type: 'image_url',
|
|
515
|
+
// encode imageBlob as base64
|
|
516
|
+
image_url: `data:image/png;base64,${btoa(
|
|
517
|
+
new Uint8Array(await imageBlob.arrayBuffer()).reduce(
|
|
518
|
+
(data, byte) => data + String.fromCharCode(byte),
|
|
519
|
+
''
|
|
520
|
+
)
|
|
521
|
+
)}`,
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
new Notice(response.choices[0].message.content ?? 'no response content...')
|
|
529
|
+
new Notice(
|
|
530
|
+
`prompt_tokens: ${response.usage?.prompt_tokens} // completion_tokens: ${response.usage?.completion_tokens} // total_tokens: ${response.usage?.total_tokens}`
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
// gpt-3.5-turbo-0125
|
|
534
|
+
// alt text, caption, and title for the image, based on the description of the image
|
|
535
|
+
const completion = await openai.chat.completions.create({
|
|
536
|
+
model: 'gpt-3.5-turbo-0125',
|
|
537
|
+
messages: [
|
|
538
|
+
{
|
|
539
|
+
role: 'user',
|
|
540
|
+
content: `You are an SEO expert and you are writing alt text, caption, and title for this image. The description of the image is: ${response.choices[0].message.content}.
|
|
541
|
+
Give me a title (name) for this image, an SEO-friendly alternative text, and a caption for this image.
|
|
542
|
+
Generate this information and respond with a JSON object using the following fields: name, alternativeText, caption.
|
|
543
|
+
Use this JSON template: {"name": "string", "alternativeText": "string", "caption": "string"}.`,
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
max_tokens: 750,
|
|
547
|
+
n: 1,
|
|
548
|
+
stop: null,
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
new Notice(
|
|
552
|
+
completion.choices[0].message.content ?? 'no response content...'
|
|
553
|
+
)
|
|
554
|
+
new Notice(
|
|
555
|
+
`prompt_tokens: ${completion.usage?.prompt_tokens} // completion_tokens: ${completion.usage?.completion_tokens} // total_tokens: ${completion.usage?.total_tokens}`
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return JSON.parse(completion.choices[0].message.content?.trim() || '{}')
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* The settings tab for the plugin
|
|
564
|
+
*/
|
|
565
|
+
class MyExportSettingTab extends PluginSettingTab {
|
|
566
|
+
plugin: MyPlugin
|
|
567
|
+
|
|
568
|
+
constructor(app: App, plugin: MyPlugin) {
|
|
569
|
+
super(app, plugin)
|
|
570
|
+
this.plugin = plugin
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
display(): void {
|
|
574
|
+
const { containerEl } = this
|
|
575
|
+
containerEl.empty()
|
|
576
|
+
|
|
577
|
+
/** ****************************************************************************
|
|
578
|
+
* Add the settings for the plugin
|
|
579
|
+
* *****************************************************************************
|
|
580
|
+
*/
|
|
581
|
+
new Setting(containerEl)
|
|
582
|
+
.setName('Strapi URL')
|
|
583
|
+
.setDesc('Enter your Strapi instance URL')
|
|
584
|
+
.addText(text =>
|
|
585
|
+
text
|
|
586
|
+
.setPlaceholder('https://your-strapi-url')
|
|
587
|
+
.setValue(this.plugin.settings.strapiUrl)
|
|
588
|
+
.onChange(async value => {
|
|
589
|
+
this.plugin.settings.strapiUrl = value
|
|
590
|
+
await this.plugin.saveSettings()
|
|
591
|
+
})
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
new Setting(containerEl)
|
|
595
|
+
.setName('Strapi API Token')
|
|
596
|
+
.setDesc('Enter your Strapi API token')
|
|
597
|
+
.addText(text =>
|
|
598
|
+
text
|
|
599
|
+
.setPlaceholder('Enter your token')
|
|
600
|
+
.setValue(this.plugin.settings.strapiApiToken)
|
|
601
|
+
.onChange(async value => {
|
|
602
|
+
this.plugin.settings.strapiApiToken = value
|
|
603
|
+
await this.plugin.saveSettings()
|
|
604
|
+
})
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
new Setting(containerEl)
|
|
608
|
+
.setName('OpenAI API Key')
|
|
609
|
+
.setDesc('Enter your OpenAI API key for GPT-3')
|
|
610
|
+
.addText(text =>
|
|
611
|
+
text
|
|
612
|
+
.setPlaceholder('Enter your OpenAI API key')
|
|
613
|
+
.setValue(this.plugin.settings.openaiApiKey)
|
|
614
|
+
.onChange(async value => {
|
|
615
|
+
this.plugin.settings.openaiApiKey = value
|
|
616
|
+
await this.plugin.saveSettings()
|
|
617
|
+
})
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
new Setting(containerEl)
|
|
621
|
+
.setName('JSON Template')
|
|
622
|
+
.setDesc('Enter the JSON template for the fields needed')
|
|
623
|
+
.addTextArea(text =>
|
|
624
|
+
text
|
|
625
|
+
.setPlaceholder('Enter your JSON template')
|
|
626
|
+
.setValue(this.plugin.settings.jsonTemplate)
|
|
627
|
+
.onChange(async value => {
|
|
628
|
+
this.plugin.settings.jsonTemplate = value
|
|
629
|
+
await this.plugin.saveSettings()
|
|
630
|
+
})
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
new Setting(containerEl)
|
|
634
|
+
.setName('JSON Template Description')
|
|
635
|
+
.setDesc('Enter the description for each field in the JSON template')
|
|
636
|
+
.addTextArea(text =>
|
|
637
|
+
text
|
|
638
|
+
.setPlaceholder('Enter the field descriptions')
|
|
639
|
+
.setValue(this.plugin.settings.jsonTemplateDescription)
|
|
640
|
+
.onChange(async value => {
|
|
641
|
+
this.plugin.settings.jsonTemplateDescription = value
|
|
642
|
+
await this.plugin.saveSettings()
|
|
643
|
+
})
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
new Setting(containerEl)
|
|
647
|
+
.setName('Strapi Article Create URL')
|
|
648
|
+
.setDesc('Enter the URL to create articles in Strapi')
|
|
649
|
+
.addText(text =>
|
|
650
|
+
text
|
|
651
|
+
.setPlaceholder('https://your-strapi-url/api/articles')
|
|
652
|
+
.setValue(this.plugin.settings.strapiArticleCreateUrl)
|
|
653
|
+
.onChange(async value => {
|
|
654
|
+
this.plugin.settings.strapiArticleCreateUrl = value
|
|
655
|
+
await this.plugin.saveSettings()
|
|
656
|
+
})
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
new Setting(containerEl)
|
|
660
|
+
.setName('Strapi Content Attribute Name')
|
|
661
|
+
.setDesc('Enter the attribute name for the content field in Strapi')
|
|
662
|
+
.addText(text =>
|
|
663
|
+
text
|
|
664
|
+
.setPlaceholder('content')
|
|
665
|
+
.setValue(this.plugin.settings.strapiContentAttributeName)
|
|
666
|
+
.onChange(async value => {
|
|
667
|
+
this.plugin.settings.strapiContentAttributeName = value
|
|
668
|
+
await this.plugin.saveSettings()
|
|
669
|
+
})
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
new Setting(containerEl)
|
|
673
|
+
.setName('Additional Prompt')
|
|
674
|
+
.setDesc(
|
|
675
|
+
'Enter an optional additional prompt to customize the article content generation'
|
|
676
|
+
)
|
|
677
|
+
.addTextArea(text =>
|
|
678
|
+
text
|
|
679
|
+
.setPlaceholder('Enter your additional prompt here...')
|
|
680
|
+
.setValue(this.plugin.settings.additionalPrompt)
|
|
681
|
+
.onChange(async value => {
|
|
682
|
+
this.plugin.settings.additionalPrompt = value
|
|
683
|
+
await this.plugin.saveSettings()
|
|
684
|
+
})
|
|
685
|
+
)
|
|
686
|
+
}
|
|
687
|
+
}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "notes-to-strapi-export-article-ai",
|
|
3
|
+
"name": "Strapi Exporter, Notes to Strapi article AI enhanced",
|
|
4
|
+
"version": "1.0.3",
|
|
5
|
+
"minAppVersion": "1.5.0",
|
|
6
|
+
"description": "Effortlessly export your notes to Strapi CMS with AI-powered image handling and SEO optimization. Replace all the images in your notes by uploaded images in Strapi, and add SEO metadata to uploaded images.",
|
|
7
|
+
"author": "Cinquin Andy",
|
|
8
|
+
"authorUrl": "https://andy.cinquin.com",
|
|
9
|
+
"isDesktopOnly": true,
|
|
10
|
+
"fundingUrl": "https://github.com/sponsors/CinquinAndy"
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "notes-to-strapi-export-article-ai",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Effortlessly export your Obsidian notes to Strapi CMS with AI-powered image handling and SEO optimization. Replace all the images in your notes by uploaded images in Strapi, and add SEO metadata to uploaded images.",
|
|
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": "Cinquin Andy",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "20.11.30",
|
|
15
|
+
"@typescript-eslint/eslint-plugin": "7.3.1",
|
|
16
|
+
"@typescript-eslint/parser": "7.3.1",
|
|
17
|
+
"builtin-modules": "3.3.0",
|
|
18
|
+
"esbuild": "0.20.2",
|
|
19
|
+
"obsidian": "latest",
|
|
20
|
+
"prettier": "3.2.5",
|
|
21
|
+
"tslib": "2.6.2",
|
|
22
|
+
"typescript": "5.4.3"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"openai": "^4.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
singleQuote: true,
|
|
3
|
+
semi: false,
|
|
4
|
+
trailingComma: 'es5',
|
|
5
|
+
tabWidth: 2,
|
|
6
|
+
useTabs: true,
|
|
7
|
+
printWidth: 80,
|
|
8
|
+
arrowParens: 'avoid',
|
|
9
|
+
bracketSpacing: true,
|
|
10
|
+
jsxSingleQuote: false,
|
|
11
|
+
proseWrap: 'preserve',
|
|
12
|
+
quoteProps: 'as-needed',
|
|
13
|
+
requirePragma: false,
|
|
14
|
+
insertPragma: false,
|
|
15
|
+
endOfLine: 'lf',
|
|
16
|
+
htmlWhitespaceSensitivity: 'css',
|
|
17
|
+
vueIndentScriptAndStyle: false,
|
|
18
|
+
embeddedLanguageFormatting: 'auto',
|
|
19
|
+
}
|
package/renovate.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": [
|
|
4
|
+
"config:base"
|
|
5
|
+
],
|
|
6
|
+
"packageRules": [
|
|
7
|
+
{
|
|
8
|
+
"matchUpdateTypes": [
|
|
9
|
+
"minor",
|
|
10
|
+
"patch"
|
|
11
|
+
],
|
|
12
|
+
"matchCurrentVersion": "!/^0/",
|
|
13
|
+
"automerge": true,
|
|
14
|
+
"automergeType": "pr",
|
|
15
|
+
"automergeStrategy": "squash"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/styles.css
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"inlineSourceMap": true,
|
|
5
|
+
"inlineSources": true,
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"target": "ES6",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"noImplicitAny": false,
|
|
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
|
+
}
|
package/version-bump.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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"));
|
package/versions.json
ADDED