notes-to-strapi-export-article-ai 1.0.119 → 3.0.0

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.
Files changed (46) hide show
  1. package/.eslintrc +30 -22
  2. package/README.md +98 -143
  3. package/images/img.png +0 -0
  4. package/images/img_1.png +0 -0
  5. package/images/img_10.png +0 -0
  6. package/images/img_11.png +0 -0
  7. package/images/img_12.png +0 -0
  8. package/images/img_13.png +0 -0
  9. package/images/img_2.png +0 -0
  10. package/images/img_3.png +0 -0
  11. package/images/img_4.png +0 -0
  12. package/images/img_5.png +0 -0
  13. package/images/img_6.png +0 -0
  14. package/images/img_7.png +0 -0
  15. package/images/img_8.png +0 -0
  16. package/images/img_9.png +0 -0
  17. package/manifest.json +2 -2
  18. package/package.json +29 -26
  19. package/src/components/APIKeys.ts +219 -0
  20. package/src/components/Configuration.ts +663 -0
  21. package/src/components/Dashboard.ts +184 -0
  22. package/src/components/ImageSelectionModal.ts +58 -0
  23. package/src/components/Routes.ts +279 -0
  24. package/src/constants.ts +22 -61
  25. package/src/main.ts +177 -34
  26. package/src/services/configuration-generator.ts +172 -0
  27. package/src/services/field-analyzer.ts +84 -0
  28. package/src/services/frontmatter.ts +329 -0
  29. package/src/services/strapi-export.ts +436 -0
  30. package/src/settings/UnifiedSettingsTab.ts +206 -0
  31. package/src/types/image.ts +27 -16
  32. package/src/types/index.ts +3 -0
  33. package/src/types/route.ts +51 -0
  34. package/src/types/settings.ts +22 -23
  35. package/src/utils/analyse-file.ts +94 -0
  36. package/src/utils/debounce.ts +34 -0
  37. package/src/utils/image-processor.ts +124 -400
  38. package/src/utils/preview-modal.ts +265 -0
  39. package/src/utils/process-file.ts +122 -0
  40. package/src/utils/strapi-uploader.ts +120 -119
  41. package/src/settings.ts +0 -404
  42. package/src/types/article.ts +0 -8
  43. package/src/utils/openai-generator.ts +0 -139
  44. package/src/utils/validators.ts +0 -8
  45. package/version-bump.mjs +0 -14
  46. package/versions.json +0 -119
package/.eslintrc CHANGED
@@ -1,24 +1,32 @@
1
1
  {
2
- "root": true,
3
- "parser": "@typescript-eslint/parser",
4
- "env": { "node": true },
5
- "plugins": [
6
- "@typescript-eslint"
2
+ "root": true,
3
+ "parser": "@typescript-eslint/parser",
4
+ "env": {
5
+ "node": true
6
+ },
7
+ "plugins": [
8
+ "@typescript-eslint"
9
+ ],
10
+ "extends": [
11
+ "eslint:recommended",
12
+ "plugin:@typescript-eslint/eslint-recommended",
13
+ "plugin:@typescript-eslint/recommended"
14
+ ],
15
+ "parserOptions": {
16
+ "sourceType": "module"
17
+ },
18
+ "rules": {
19
+ "no-unused-vars": "off",
20
+ "@typescript-eslint/no-unused-vars": [
21
+ "error",
22
+ {
23
+ "args": "none"
24
+ }
7
25
  ],
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
- }
26
+ "@typescript-eslint/ban-ts-comment": "off",
27
+ "no-prototype-builtins": "off",
28
+ "@typescript-eslint/no-empty-function": "off",
29
+ "@typescript-eslint/no-explicit-any": "off",
30
+ "no-extra-semi": "off"
31
+ }
32
+ }
package/README.md CHANGED
@@ -6,15 +6,28 @@
6
6
 
7
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
8
 
9
- ## ✨ Features
9
+ ## ✨ Key Features
10
+
11
+ - 🔄 Multiple Export Routes
12
+ - Configure different export paths for various content types
13
+ - Customizable field mappings per route
14
+ - Support for multiple languages
15
+
16
+ - 🤖 AI-Powered Content Processing
17
+ - Automatic frontmatter generation
18
+ - SEO optimization
19
+ - Content analysis and enhancement
20
+
21
+ - 📷 Advanced Image Management
22
+ - Automatic image upload to Strapi
23
+ - Support for both single images and galleries
24
+ - Image path management and updates
25
+
26
+ - 🔧 Flexible Configuration
27
+ - JSON schema-based configuration
28
+ - Custom field mappings
29
+ - Language-specific settings
10
30
 
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
- - 📷 Supports both single images and image galleries
17
- - 🔗 Additional API route for creating content in a different Strapi content type
18
31
 
19
32
  ## 🛠️ Installation
20
33
 
@@ -58,159 +71,101 @@ To install Strapi Exporter, follow these steps (coming soon to the Obsidian plug
58
71
  ## ⚙️ Configuration
59
72
 
60
73
  To get started with Strapi Exporter, you'll need to configure the following settings:
61
-
62
- - **Strapi URL**: The URL of your Strapi instance (e.g., `https://your-strapi-url`).
63
- ![img.png](images/img.png)
64
- - **Strapi API Token**: Your Strapi API token for authentication. You can create an API token in your Strapi admin panel under "Settings" > "API Tokens".
65
- ![img_1.png](images/img_1.png)
66
- ![img_2.png](images/img_2.png)
67
- - You need, at least, to have the following permissions:
68
- - article: create
69
- - upload: create
70
- - (you can also add full permissions, but it's not really recommended for security reasons)
71
- ![img_3.png](images/img_3.png)
72
- - (the token in the screenshot is not valid, don't try to use it 😌)
73
- - **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).
74
- ![img_4.png](images/img_4.png)
75
- - this key is needed to use the GPT-3 API, which is used to generate the content of the article
76
- - (it need to access to "Model capabilities" with "write" permission)
77
- ![img_5.png](images/img_5.png)
78
- - (or with the "all" permission)
79
- - **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).
80
- ![img_6.png](images/img_6.png)
81
- - to get the JSON template, you can go to the documentation of your Strapi API, and copy the JSON template of the article creation
82
- - it should look like this: `https://{api_url}/documentation/v1.0.0`
83
- - then, go to the article creation, and copy the JSON template
84
- ![img_7.png](images/img_7.png)
85
- ![img_8.png](images/img_8.png)
86
- - it should look like this for example:
87
-
88
- ```json
89
- {
90
- "data": {
91
- "title": "string",
92
- "seo_title": "string",
93
- "seo_description": "string",
94
- "slug": "string",
95
- "excerpt": "string",
96
- "links": [
97
- {
98
- "id": "number",
99
- "label": "string",
100
- "url": "string"
101
- }
102
- ],
103
- "subtitle": "string",
104
- "type": "string",
105
- "rank": "number",
106
- "tags": [
107
- {
108
- "id": "number",
109
- "name": "string"
110
- }
111
- ],
112
- "locale": "string"
113
- }
114
- }
74
+ ![img.png](images/img.png)
75
+
76
+ ### 1. Go in "API Keys" tab
77
+ - (required) Add your Strapi API URL
78
+ It's the link to your Strapi API, for example: `https://api.yourdomain.com`
79
+ ![img_1.png](images/img_1.png)
80
+ - (required) Add your Strapi API Token
81
+ ![img_2.png](images/img_2.png)
82
+ (settings -> api tokens -> create new API Token)
83
+ then, give it the right permissions (at least, upload, and create permissions on the content type you want to use)
84
+ (or just use a full-access token [in Token type configuration])
85
+ - (required) Add your OpenAI API Token
86
+ ![img_3.png](images/img_3.png)
87
+ you need to create an account on OpenAI, and create an API key, then add it here
88
+ - (optional to improve the results) Add your ForVoyez API Token
89
+
90
+ ### 2. Go in "Routes" tab
91
+ - Add a new route
92
+ - Give it a name, icon, url, subtitle
93
+ - for example,
94
+ - name : articles
95
+ - icon : bird
96
+ - url : /api/articles
97
+ - subtitle : Create articles
98
+ - You can add multiple routes, for example, one for articles, one for pages, one for categories, etc.
99
+ ![img_4.png](images/img_4.png)
100
+
101
+ ### 3. Go in "Configuration" tab
102
+ - For each route, you can configure the fields you want to use, and the schema needed for the fields
103
+ in Strapi, go in Documentation -> Open Documentation
104
+ ![img_5.png](images/img_5.png)
105
+ You will see the openapi schema, you can use it to create the schema for the fields you want to use
106
+ ![img_6.png](images/img_6.png)
107
+ You need to check the POST method, and the schema needed for the fields you want to use
108
+ ![img_7.png](images/img_7.png)
109
+ Copy the whole schema in the Strapi schema section in the obsidian plugin
110
+ ![img_8.png](images/img_8.png)
111
+
112
+ Then, you need to describe the content you want to use, and you want to upload at the end,
113
+ in schema description, for exemple for me :
114
+ in Strapi Schema
115
115
  ```
116
-
117
- - then, you can copy this JSON template in the settings of the plugin
118
- - and copy that, to describe each field in the other JSON description setting
119
- - **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.
120
-
121
- ```json
122
116
  {
123
- "data": {
124
- "title": "<Title of the item, as a short string>",
125
- "seo_title": "<SEO optimized title, as a short string>",
126
- "seo_description": "<SEO optimized description, as a short string>",
127
- "slug": "<URL-friendly string derived from the title>",
128
- "excerpt": "<A short preview or snippet from the content>",
129
- "links": [
130
- {
131
- "id": "<Unique identifier for the link, as a number>",
132
- "label": "<Display text for the link, as a short string>",
133
- "url": "<URL the link points to, as a string>"
134
- }
135
- ],
136
- "subtitle": "<Subtitle or secondary title, as a short string>",
137
- "type": "<Category or type of the item, as a short string>",
138
- "rank": "<Numerical ranking or order priority, as a number>",
139
- "tags": [
140
- {
141
- "id": "<Unique identifier for the tag, as a number>",
142
- "name": "<Name of the tag, as a short string>"
143
- }
144
- ],
145
- "locale": "<Locale or language code, as a short string>"
146
- }
147
- }
117
+ "data": {
118
+ "title": "string",
119
+ "content": "string",
120
+ "seo_title": "string",
121
+ "seo_description": "string",
122
+ "slug": "string",
123
+ ...
124
+ }
148
125
  ```
149
-
126
+ In Strapi Description (just describe with your words the fields, to help the AI to understand what you want) :
150
127
  ```
151
- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
152
- ⚠️
153
- ⚠️ **Important:** Remove the `content` (or the attribute name that correspond to the main content of the article)
154
- ⚠️ field from the JSON template and specify it separately in the "Strapi Content Attribute Name" setting.
155
- ⚠️ ( i do that to avoid changes on the main content by chat gpt )
156
- ⚠️
157
- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
128
+ {
129
+ "data": {
130
+ "title": "the title of the article",
131
+ "content": "the content normal of the article",
132
+ "seo_title": "the seo optimized version of the title",
133
+ "seo_description": "a summary of the content, seo optimized, for the seo optimized description",
134
+ "slug": "the-slug-for-the-access-of-the-article",
135
+ ...
136
+ }
158
137
  ```
138
+ ![img_9.png](images/img_9.png)
159
139
 
160
- - **Strapi Article Create URL**: The URL to create articles in Strapi (e.g., `https://your-strapi-url/api/articles`).
161
- - to get the URL, you can go to the documentation of your Strapi API, and copy the URL of the article creation
162
- - it should look like this: `https://{api_url}/documentation/v1.0.0`
163
- - then, go to the article creation, and copy the URL
164
- - **Strapi Content Attribute Name**: The attribute name for the content field in Strapi (e.g., `content`).
165
- - 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)
166
- - **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.
167
-
168
- ### Image Settings (main), optional
169
-
170
- - **Enable Additional Call API Image**: Toggle this option to enable the image feature for the additional API route. The plugin will look for an image in the `image` folder next to your article file and upload it to Strapi.
171
- - **Additional Call API Image Full Path Property**: Specify the full path to the image property for the additional API route in your Strapi API (e.g., `image_presentation`).
140
+ - And now, in the content field name, you need to write the main content attribute name, for example, in my case, it's "content"
141
+ it because we want to avoid the AI to rewrite the content field. (it's already written by us)
172
142
 
173
- - **Enable Additional Call API Gallery**: Toggle this option to enable the gallery feature for the additional API route. The plugin will look for images in the `gallery` folder next to your article file and upload them to Strapi.
174
- - **Additional Call API Gallery Full Path Property**: Specify the full path to the gallery property for the additional API route in your Strapi API (e.g., `gallery`).
175
- -
143
+ ![img_10.png](images/img_10.png)
176
144
 
177
- ### Additional API Route Settings
145
+ - Select the target Language.
146
+ ![img_11.png](images/img_11.png)
178
147
 
179
- ⚠️ Warning: When enabling the additional API route feature, you'll need to restart Obsidian to see the additional button in the ribbon menu.
148
+ - click on "Generate Configuration", and wait the configuration to be generated
149
+ - check if it's coherent with what you want, and click on "Apply"
180
150
 
181
- - **Enable Additional Call API**: Toggle this option to enable the additional API route feature. This will display a new icon in the ribbon menu for creating content in a different Strapi content type.
182
- - **Additional JSON Template**: Enter the JSON template for the fields needed for the additional API route.
183
- - **Additional API JSON Template Description**: Enter the description for each field in the additional API JSON template.
184
- - **Additional API URL**: Enter the URL to create content for the additional API route (e.g., `https://your-strapi-url/api/additional-content`).
185
- - **Additional API Content Attribute Name**: Enter the attribute name for the content field for the additional API route.
151
+ ![img_12.png](images/img_12.png)
186
152
 
187
- ### Image Settings (additional), optional
153
+ - Repeat the process for each route you want to use, (you can select an another route with the dropdown menu [select route])
154
+ - Reload the plugin (ctrl + r) to apply the changes
188
155
 
189
- - **Enable Additional Call API Image**: Toggle this option to enable the image feature for the additional API route. The plugin will look for an image in the `image` folder next to your article file and upload it to Strapi.
190
- - **Additional Call API Image Full Path Property**: Specify the full path to the image property for the additional API route in your Strapi API (e.g., `image_presentation`).
191
-
192
- - **Enable Additional Call API Gallery**: Toggle this option to enable the gallery feature for the additional API route. The plugin will look for images in the `gallery` folder next to your article file and upload them to Strapi.
193
- - **Additional Call API Gallery Full Path Property**: Specify the full path to the gallery property for the additional API route in your Strapi API (e.g., `gallery`).
194
156
 
195
157
  ## 🚀 Usage
196
158
 
159
+ YOU NEED TO HAVE FINISHED THE CONFIGURATION BEFORE USING THE PLUGIN
160
+
197
161
  1. Open a Markdown file in Obsidian.
198
162
  2. Click on the plugin's ribbon icon to start the magic.
199
- ![img_9.png](images/img_9.png)
200
- 3. Sit back and relax while Strapi Exporter does the heavy lifting:
201
- - 🖼️ Extracting and uploading images to Strapi
202
- - 🎨 Generating SEO-friendly alt text and captions for images
203
- - 📝 Creating SEO-optimized article content based on your notes
204
- - 🌐 Publishing the article to Strapi with the generated content and images
205
- 4. Enjoy your freshly exported article in Strapi!
206
-
207
- For the plugin to detect images and galleries, ensure the following folder structure:
208
-
209
- - Article file (e.g., `article.md`)
210
- - Main image folder (name: `image`)
211
- - Main gallery folder (name: `gallery`)
163
+ ![img_13.png](images/img_13.png)
164
+ 3. if you haven't any frontmatter, click on "Generate" to generate the frontmatter
165
+ 4. you can change the frontmatter if you want, directly in the file
166
+ 5. Check if the frontmatter is correct, and click on "Confirm & Export"
167
+ 6. Enjoy your freshly exported article in Strapi!
212
168
 
213
- The plugin will detect images in the `image` and `gallery` folders and upload them to Strapi.
214
169
 
215
170
  ## 🤝 Contributing
216
171
 
package/images/img.png CHANGED
Binary file
package/images/img_1.png CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
package/images/img_2.png CHANGED
Binary file
package/images/img_3.png CHANGED
Binary file
package/images/img_4.png CHANGED
Binary file
package/images/img_5.png CHANGED
Binary file
package/images/img_6.png CHANGED
Binary file
package/images/img_7.png CHANGED
Binary file
package/images/img_8.png CHANGED
Binary file
package/images/img_9.png CHANGED
Binary file
package/manifest.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "notes-to-strapi-export-article-ai",
3
3
  "name": "Strapi Exporter AI",
4
- "version": "1.0.119",
5
- "minAppVersion": "1.5.0",
4
+ "version": "3.0.0",
5
+ "minAppVersion": "1.7.0",
6
6
  "description": "Effortlessly export your notes to Strapi CMS with AI-powered handling and SEO optimization.",
7
7
  "author": "Cinquin Andy",
8
8
  "authorUrl": "https://andy.cinquin.com",
package/package.json CHANGED
@@ -1,28 +1,31 @@
1
1
  {
2
- "name": "notes-to-strapi-export-article-ai",
3
- "version": "1.0.119",
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.14.14",
15
- "@typescript-eslint/eslint-plugin": "7.18.0",
16
- "@typescript-eslint/parser": "7.18.0",
17
- "builtin-modules": "3.3.0",
18
- "esbuild": "0.20.2",
19
- "obsidian": "latest",
20
- "prettier": "3.3.3",
21
- "tslib": "2.6.3",
22
- "typescript": "5.5.4"
23
- },
24
- "dependencies": {
25
- "openai": "^4.0.0",
26
- "process": "^0.11.10"
27
- }
2
+ "name": "notes-to-strapi-export-article-ai",
3
+ "version": "3.0.0",
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": "22.9.0",
15
+ "@typescript-eslint/eslint-plugin": "8.13.0",
16
+ "@typescript-eslint/parser": "8.13.0",
17
+ "builtin-modules": "4.0.0",
18
+ "esbuild": "0.24.0",
19
+ "obsidian": "latest",
20
+ "prettier": "3.3.3",
21
+ "tslib": "2.8.1",
22
+ "typescript": "5.6.3"
23
+ },
24
+ "dependencies": {
25
+ "@ai-sdk/openai": "0.0.72",
26
+ "ai": "3.4.33",
27
+ "js-yaml": "4.1.0",
28
+ "openai": "4.71.1",
29
+ "process": "0.11.10"
30
+ }
28
31
  }
@@ -0,0 +1,219 @@
1
+ import { Setting, Notice } from 'obsidian'
2
+ import StrapiExporterPlugin from '../main'
3
+
4
+ /**
5
+ * Represents a field validator for API keys
6
+ */
7
+ interface APIKeyValidator {
8
+ validate: (key: string) => boolean
9
+ pattern: RegExp
10
+ message: string
11
+ }
12
+
13
+ /**
14
+ * Class for managing API key settings in Obsidian
15
+ */
16
+ export class APIKeys {
17
+ private readonly validators: Record<string, APIKeyValidator>
18
+
19
+ constructor(
20
+ private plugin: StrapiExporterPlugin,
21
+ private containerEl: HTMLElement
22
+ ) {
23
+ this.validators = this.initializeValidators()
24
+ }
25
+
26
+ /**
27
+ * Initializes field validators
28
+ */
29
+ private initializeValidators(): Record<string, APIKeyValidator> {
30
+ return {
31
+ strapiUrl: {
32
+ validate: (url: string) => /^https?:\/\/.+/.test(url),
33
+ pattern: /^https?:\/\/.+/,
34
+ message: 'Must be a valid URL starting with http:// or https://',
35
+ },
36
+ strapiApiToken: {
37
+ validate: (token: string) => token.length >= 32,
38
+ pattern: /.{32,}/,
39
+ message: 'Must be at least 32 characters long',
40
+ },
41
+ openaiApiKey: {
42
+ validate: (key: string) => /^sk-[A-Za-z0-9]{32,}$/.test(key),
43
+ pattern: /^sk-[A-Za-z0-9]{32,}$/,
44
+ message: 'Must start with "sk-" followed by at least 32 characters',
45
+ },
46
+ }
47
+ }
48
+
49
+ display(): void {
50
+ const { containerEl } = this
51
+ containerEl.empty()
52
+
53
+ this.createHeader()
54
+ this.addStrapiSettings()
55
+ this.addForVoyezSettings()
56
+ this.addOpenAISettings()
57
+ this.addValidationButton()
58
+ }
59
+
60
+ private createHeader(): void {
61
+ const header = this.containerEl.createEl('div', { cls: 'api-keys-header' })
62
+
63
+ header.createEl('h2', { text: 'API Keys Configuration' })
64
+ header.createEl('p', {
65
+ text: 'Configure your API keys for various services. All keys are stored locally and encrypted.',
66
+ cls: 'api-keys-description',
67
+ })
68
+ }
69
+
70
+ private addStrapiSettings(): void {
71
+ // Strapi URL Setting
72
+ this.createSettingField({
73
+ name: 'Strapi URL',
74
+ desc: 'Enter your Strapi instance URL (e.g. https://your-strapi-url)',
75
+ placeholder: 'https://your-strapi-url',
76
+ value: this.plugin.settings.strapiUrl,
77
+ settingKey: 'strapiUrl',
78
+ type: 'url',
79
+ })
80
+
81
+ // Strapi API Token Setting
82
+ this.createSettingField({
83
+ name: 'Strapi API Token',
84
+ desc: 'Enter your Strapi API token',
85
+ placeholder: 'Enter your token',
86
+ value: this.plugin.settings.strapiApiToken,
87
+ settingKey: 'strapiApiToken',
88
+ type: 'password',
89
+ })
90
+ }
91
+
92
+ private addForVoyezSettings(): void {
93
+ this.createSettingField({
94
+ name: 'ForVoyez API Key',
95
+ desc: 'Enter your ForVoyez API key',
96
+ placeholder: 'Enter your ForVoyez API key',
97
+ value: this.plugin.settings.forvoyezApiKey,
98
+ settingKey: 'forvoyezApiKey',
99
+ type: 'password',
100
+ })
101
+ }
102
+
103
+ private addOpenAISettings(): void {
104
+ this.createSettingField({
105
+ name: 'OpenAI API Key',
106
+ desc: 'Enter your OpenAI API key',
107
+ placeholder: 'Enter your OpenAI API key',
108
+ value: this.plugin.settings.openaiApiKey,
109
+ settingKey: 'openaiApiKey',
110
+ type: 'password',
111
+ })
112
+ }
113
+
114
+ private createSettingField({
115
+ name,
116
+ desc,
117
+ placeholder,
118
+ value,
119
+ settingKey,
120
+ type,
121
+ }: {
122
+ name: string
123
+ desc: string
124
+ placeholder: string
125
+ value: string
126
+ settingKey: string
127
+ type: 'text' | 'password' | 'url'
128
+ }): void {
129
+ const setting = new Setting(this.containerEl).setName(name).setDesc(desc)
130
+
131
+ // Add validation message element first
132
+ const validationEl = setting.settingEl.createDiv('validation-message')
133
+ validationEl.style.display = 'none'
134
+
135
+ // Create text component
136
+ setting.addText(text => {
137
+ // Configure text component
138
+ text
139
+ .setPlaceholder(placeholder)
140
+ .setValue(value)
141
+ .onChange(async newValue => {
142
+ try {
143
+ await this.updateSetting(settingKey, newValue)
144
+ } catch (error) {
145
+ new Notice(`Failed to update ${name}: ${error.message}`)
146
+ }
147
+ })
148
+
149
+ // Handle password type
150
+ if (type === 'password') {
151
+ text.inputEl.type = 'password'
152
+ text.inputEl.classList.add('api-key-input')
153
+ }
154
+
155
+ // Add blur event listener directly
156
+ text.inputEl.addEventListener('blur', () => {
157
+ this.validateField(settingKey, text.getValue(), validationEl)
158
+ })
159
+
160
+ return text
161
+ })
162
+ }
163
+
164
+ private async updateSetting(key: string, value: string): Promise<void> {
165
+ // Validate if we have a validator for this key
166
+ if (this.validators[key]) {
167
+ const isValid = this.validators[key].validate(value)
168
+ if (!isValid) {
169
+ throw new Error(this.validators[key].message)
170
+ }
171
+ }
172
+
173
+ // Update the setting
174
+ this.plugin.settings[key] = value
175
+ await this.plugin.saveSettings()
176
+ }
177
+
178
+ private validateField(
179
+ key: string,
180
+ value: string,
181
+ validationEl: HTMLElement
182
+ ): void {
183
+ const validator = this.validators[key]
184
+ if (validator) {
185
+ const isValid = validator.validate(value)
186
+ validationEl.style.display = isValid ? 'none' : 'block'
187
+ validationEl.setText(isValid ? '' : validator.message)
188
+ validationEl.classList.toggle('invalid', !isValid)
189
+ }
190
+ }
191
+
192
+ private addValidationButton(): void {
193
+ new Setting(this.containerEl).addButton(button =>
194
+ button
195
+ .setButtonText('Validate All Keys')
196
+ .setCta()
197
+ .onClick(async () => {
198
+ try {
199
+ await this.validateAllKeys()
200
+ } catch (error) {
201
+ new Notice(`Validation failed: ${error.message}`)
202
+ }
203
+ })
204
+ )
205
+ }
206
+
207
+ private async validateAllKeys(): Promise<void> {
208
+ const validations = Object.entries(this.validators).map(
209
+ async ([key, validator]) => {
210
+ const value = this.plugin.settings[key]
211
+ if (!validator.validate(value)) {
212
+ throw new Error(`Invalid ${key}: ${validator.message}`)
213
+ }
214
+ }
215
+ )
216
+
217
+ await Promise.all(validations)
218
+ }
219
+ }