notes-to-strapi-export-article-ai 1.0.11 → 1.0.12

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/main.ts DELETED
@@ -1,691 +0,0 @@
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 Strapi Exporter plugin
14
- */
15
- interface StrapiExporterSettings {
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_STRAPI_EXPORTER_SETTINGS: StrapiExporterSettings = {
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 StrapiExporterPlugin extends Plugin {
83
- settings: StrapiExporterSettings
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('strapi-exporter-ribbon-class')
322
-
323
- this.addSettingTab(new StrapiExporterSettingTab(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(
333
- {},
334
- DEFAULT_STRAPI_EXPORTER_SETTINGS,
335
- await this.loadData()
336
- )
337
- }
338
-
339
- /**
340
- * Save the settings for the plugin
341
- */
342
- async saveSettings() {
343
- await this.saveData(this.settings)
344
- }
345
-
346
- /**
347
- * Extract the image paths from the content
348
- * @param content
349
- */
350
- extractImagePaths(content: string): string[] {
351
- /**
352
- * Extract the image paths from the content
353
- */
354
- const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
355
- const imagePaths: string[] = []
356
- let match
357
-
358
- while ((match = imageRegex.exec(content)) !== null) {
359
- imagePaths.push(match[1])
360
- }
361
-
362
- return imagePaths
363
- }
364
-
365
- /**
366
- * Check if the content has any unexported images
367
- * @param content
368
- */
369
- hasUnexportedImages(content: string): boolean {
370
- const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
371
- return imageRegex.test(content)
372
- }
373
-
374
- /**
375
- * Get the image blobs from the image paths
376
- * @param imagePaths
377
- */
378
- async getImageBlobs(
379
- imagePaths: string[]
380
- ): Promise<{ path: string; blob: Blob; name: string }[]> {
381
- // get all the files in the vault
382
- const files = this.app.vault.getAllLoadedFiles()
383
- // get the image files name from the vault
384
- const fileNames = files.map(file => file.name)
385
- // filter the image files, and get all the images files paths
386
- const imageFiles = imagePaths.filter(path => fileNames.includes(path))
387
- // get the image blobs, find it, and return the blob
388
- return await Promise.all(
389
- imageFiles.map(async path => {
390
- const file = files.find(file => file.name === path)
391
- if (file instanceof TFile) {
392
- const blob = await this.app.vault.readBinary(file)
393
- return {
394
- name: path,
395
- blob: new Blob([blob], { type: 'image/png' }),
396
- path: file.path,
397
- }
398
- }
399
- return {
400
- name: '',
401
- blob: new Blob(),
402
- path: '',
403
- }
404
- })
405
- )
406
- }
407
-
408
- /**
409
- * Upload the images to Strapi
410
- * @param imageBlobs
411
- */
412
- async uploadImagesToStrapi(
413
- imageBlobs: {
414
- path: string
415
- blob: Blob
416
- name: string
417
- description: {
418
- name: string
419
- alternativeText: string
420
- caption: string
421
- }
422
- }[]
423
- ): Promise<{ [key: string]: { url: string; data: any } }> {
424
- // upload the images to Strapi
425
- const uploadedImages: {
426
- [key: string]: { url: string; data: any }
427
- } = {}
428
-
429
- /**
430
- * Upload the images to Strapi
431
- */
432
- for (const imageBlob of imageBlobs) {
433
- const formData = new FormData()
434
- /**
435
- * Append the image blob and the image description to the form data
436
- */
437
- formData.append('files', imageBlob.blob, imageBlob.name)
438
- formData.append(
439
- 'fileInfo',
440
- JSON.stringify({
441
- name: imageBlob.description.name,
442
- alternativeText: imageBlob.description.alternativeText,
443
- caption: imageBlob.description.caption,
444
- })
445
- )
446
-
447
- // upload the image to Strapi
448
- try {
449
- const response = await fetch(`${this.settings.strapiUrl}/api/upload`, {
450
- method: 'POST',
451
- headers: {
452
- Authorization: `Bearer ${this.settings.strapiApiToken}`,
453
- },
454
- body: formData,
455
- })
456
-
457
- /**
458
- * If the response is ok, add the uploaded image to the uploaded images object
459
- */
460
- if (response.ok) {
461
- const data = await response.json()
462
- uploadedImages[imageBlob.name] = {
463
- url: data[0].url,
464
- data: data[0],
465
- }
466
- } else {
467
- new Notice(`Failed to upload image: ${imageBlob.name}`)
468
- }
469
- } catch (error) {
470
- new Notice(`Error uploading image: ${imageBlob.name}`)
471
- }
472
- }
473
-
474
- return uploadedImages
475
- }
476
-
477
- /**
478
- * Replace the image paths in the content with the uploaded image URLs
479
- * @param content
480
- * @param uploadedImages
481
- */
482
- replaceImagePaths(
483
- content: string,
484
- uploadedImages: { [key: string]: { url: string; data: any } }
485
- ): string {
486
- /**
487
- * Replace the image paths in the content with the uploaded image URLs
488
- */
489
- for (const [localPath, imageData] of Object.entries(uploadedImages)) {
490
- const markdownImageRegex = new RegExp(`!\\[\\[${localPath}\\]\\]`, 'g')
491
- content = content.replace(
492
- markdownImageRegex,
493
- `![${imageData.data.alternativeText}](${imageData.url})`
494
- )
495
- }
496
- return content
497
- }
498
-
499
- /**
500
- * Get the description of the image using OpenAI
501
- * @param imageBlob
502
- * @param openai
503
- */
504
- async getImageDescription(imageBlob: Blob, openai: OpenAI) {
505
- // get the image description using the OpenAI API ( using gpt 4 vision preview model )
506
- const response = await openai.chat.completions.create({
507
- model: 'gpt-4-vision-preview',
508
- messages: [
509
- {
510
- role: 'user',
511
- // @ts-ignore
512
- content: [
513
- {
514
- type: 'text',
515
- text: `What's in this image? make it simple, i just want the context and an idea(think about alt text)`,
516
- },
517
- {
518
- type: 'image_url',
519
- // encode imageBlob as base64
520
- image_url: `data:image/png;base64,${btoa(
521
- new Uint8Array(await imageBlob.arrayBuffer()).reduce(
522
- (data, byte) => data + String.fromCharCode(byte),
523
- ''
524
- )
525
- )}`,
526
- },
527
- ],
528
- },
529
- ],
530
- })
531
-
532
- new Notice(response.choices[0].message.content ?? 'no response content...')
533
- new Notice(
534
- `prompt_tokens: ${response.usage?.prompt_tokens} // completion_tokens: ${response.usage?.completion_tokens} // total_tokens: ${response.usage?.total_tokens}`
535
- )
536
-
537
- // gpt-3.5-turbo-0125
538
- // alt text, caption, and title for the image, based on the description of the image
539
- const completion = await openai.chat.completions.create({
540
- model: 'gpt-3.5-turbo-0125',
541
- messages: [
542
- {
543
- role: 'user',
544
- 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}.
545
- Give me a title (name) for this image, an SEO-friendly alternative text, and a caption for this image.
546
- Generate this information and respond with a JSON object using the following fields: name, alternativeText, caption.
547
- Use this JSON template: {"name": "string", "alternativeText": "string", "caption": "string"}.`,
548
- },
549
- ],
550
- max_tokens: 750,
551
- n: 1,
552
- stop: null,
553
- })
554
-
555
- new Notice(
556
- completion.choices[0].message.content ?? 'no response content...'
557
- )
558
- new Notice(
559
- `prompt_tokens: ${completion.usage?.prompt_tokens} // completion_tokens: ${completion.usage?.completion_tokens} // total_tokens: ${completion.usage?.total_tokens}`
560
- )
561
-
562
- return JSON.parse(completion.choices[0].message.content?.trim() || '{}')
563
- }
564
- }
565
-
566
- /**
567
- * The settings tab for the Strapi Exporter plugin
568
- */
569
- class StrapiExporterSettingTab extends PluginSettingTab {
570
- plugin: StrapiExporterPlugin
571
-
572
- constructor(app: App, plugin: StrapiExporterPlugin) {
573
- super(app, plugin)
574
- this.plugin = plugin
575
- }
576
-
577
- display(): void {
578
- const { containerEl } = this
579
- containerEl.empty()
580
-
581
- /** ****************************************************************************
582
- * Add the settings for the plugin
583
- * *****************************************************************************
584
- */
585
- new Setting(containerEl)
586
- .setName('Strapi URL')
587
- .setDesc('Enter your Strapi instance URL')
588
- .addText(text =>
589
- text
590
- .setPlaceholder('https://your-strapi-url')
591
- .setValue(this.plugin.settings.strapiUrl)
592
- .onChange(async value => {
593
- this.plugin.settings.strapiUrl = value
594
- await this.plugin.saveSettings()
595
- })
596
- )
597
-
598
- new Setting(containerEl)
599
- .setName('Strapi API Token')
600
- .setDesc('Enter your Strapi API token')
601
- .addText(text =>
602
- text
603
- .setPlaceholder('Enter your token')
604
- .setValue(this.plugin.settings.strapiApiToken)
605
- .onChange(async value => {
606
- this.plugin.settings.strapiApiToken = value
607
- await this.plugin.saveSettings()
608
- })
609
- )
610
-
611
- new Setting(containerEl)
612
- .setName('OpenAI API Key')
613
- .setDesc('Enter your OpenAI API key for GPT-3')
614
- .addText(text =>
615
- text
616
- .setPlaceholder('Enter your OpenAI API key')
617
- .setValue(this.plugin.settings.openaiApiKey)
618
- .onChange(async value => {
619
- this.plugin.settings.openaiApiKey = value
620
- await this.plugin.saveSettings()
621
- })
622
- )
623
-
624
- new Setting(containerEl)
625
- .setName('JSON Template')
626
- .setDesc('Enter the JSON template for the fields needed')
627
- .addTextArea(text =>
628
- text
629
- .setPlaceholder('Enter your JSON template')
630
- .setValue(this.plugin.settings.jsonTemplate)
631
- .onChange(async value => {
632
- this.plugin.settings.jsonTemplate = value
633
- await this.plugin.saveSettings()
634
- })
635
- )
636
-
637
- new Setting(containerEl)
638
- .setName('JSON Template Description')
639
- .setDesc('Enter the description for each field in the JSON template')
640
- .addTextArea(text =>
641
- text
642
- .setPlaceholder('Enter the field descriptions')
643
- .setValue(this.plugin.settings.jsonTemplateDescription)
644
- .onChange(async value => {
645
- this.plugin.settings.jsonTemplateDescription = value
646
- await this.plugin.saveSettings()
647
- })
648
- )
649
-
650
- new Setting(containerEl)
651
- .setName('Strapi Article Create URL')
652
- .setDesc('Enter the URL to create articles in Strapi')
653
- .addText(text =>
654
- text
655
- .setPlaceholder('https://your-strapi-url/api/articles')
656
- .setValue(this.plugin.settings.strapiArticleCreateUrl)
657
- .onChange(async value => {
658
- this.plugin.settings.strapiArticleCreateUrl = value
659
- await this.plugin.saveSettings()
660
- })
661
- )
662
-
663
- new Setting(containerEl)
664
- .setName('Strapi Content Attribute Name')
665
- .setDesc('Enter the attribute name for the content field in Strapi')
666
- .addText(text =>
667
- text
668
- .setPlaceholder('content')
669
- .setValue(this.plugin.settings.strapiContentAttributeName)
670
- .onChange(async value => {
671
- this.plugin.settings.strapiContentAttributeName = value
672
- await this.plugin.saveSettings()
673
- })
674
- )
675
-
676
- new Setting(containerEl)
677
- .setName('Additional Prompt')
678
- .setDesc(
679
- 'Enter an optional additional prompt to customize the article content generation'
680
- )
681
- .addTextArea(text =>
682
- text
683
- .setPlaceholder('Enter your additional prompt here...')
684
- .setValue(this.plugin.settings.additionalPrompt)
685
- .onChange(async value => {
686
- this.plugin.settings.additionalPrompt = value
687
- await this.plugin.saveSettings()
688
- })
689
- )
690
- }
691
- }
package/styles.css DELETED
@@ -1,8 +0,0 @@
1
- /*
2
-
3
- This CSS file will be included with your plugin, and
4
- available in the app when your plugin is enabled.
5
-
6
- If your plugin does not need CSS, delete this file.
7
-
8
- */
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes