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
@@ -0,0 +1,663 @@
1
+ import {
2
+ App,
3
+ DropdownComponent,
4
+ Modal,
5
+ Notice,
6
+ Setting,
7
+ TextAreaComponent,
8
+ TextComponent,
9
+ TFile,
10
+ } from 'obsidian'
11
+ import StrapiExporterPlugin from '../main'
12
+ import { uploadImageToStrapi } from '../utils/strapi-uploader'
13
+ import { RouteConfig } from '../types'
14
+ import { StructuredFieldAnalyzer } from '../services/field-analyzer'
15
+ import { ConfigurationGenerator } from '../services/configuration-generator'
16
+
17
+ export class Configuration {
18
+ private fieldAnalyzer: StructuredFieldAnalyzer
19
+ private configGenerator: ConfigurationGenerator
20
+ private readonly plugin: StrapiExporterPlugin
21
+ private readonly containerEl: HTMLElement
22
+ private readonly components: {
23
+ schemaInput: TextAreaComponent | null
24
+ schemaDescriptionInput: TextAreaComponent | null
25
+ contentFieldInput: TextComponent | null
26
+ configOutput: TextAreaComponent | null
27
+ languageDropdown: DropdownComponent | null
28
+ routeSelector: DropdownComponent | null
29
+ } = {
30
+ schemaInput: null,
31
+ schemaDescriptionInput: null,
32
+ contentFieldInput: null,
33
+ configOutput: null,
34
+ languageDropdown: null,
35
+ routeSelector: null,
36
+ }
37
+ private currentRouteId: string
38
+ private readonly app: App
39
+
40
+ constructor(plugin: StrapiExporterPlugin, containerEl: HTMLElement) {
41
+ this.plugin = plugin
42
+ this.containerEl = containerEl
43
+ this.app = plugin.app
44
+ this.currentRouteId = this.plugin.settings.routes[0]?.id || ''
45
+
46
+ // Initialiser les services avec la clé API
47
+ this.initializeServices()
48
+
49
+ this.components = {
50
+ schemaInput: null,
51
+ schemaDescriptionInput: null,
52
+ contentFieldInput: null,
53
+ configOutput: null,
54
+ languageDropdown: null,
55
+ routeSelector: null,
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Initialize or reinitialize services with current API key
61
+ */
62
+ private initializeServices(): void {
63
+ if (!this.plugin.settings.openaiApiKey) {
64
+ return
65
+ }
66
+
67
+ this.fieldAnalyzer = new StructuredFieldAnalyzer({
68
+ openaiApiKey: this.plugin.settings.openaiApiKey,
69
+ })
70
+
71
+ this.configGenerator = new ConfigurationGenerator({
72
+ openaiApiKey: this.plugin.settings.openaiApiKey,
73
+ })
74
+ }
75
+
76
+ display(): void {
77
+ const { containerEl } = this
78
+ containerEl.empty()
79
+
80
+ try {
81
+ this.createHeader()
82
+ this.addRouteSelector()
83
+ this.addSchemaConfigSection()
84
+ this.addContentFieldSection()
85
+ this.addLanguageSection()
86
+ this.addAutoConfigSection()
87
+ } catch (error) {
88
+ this.showError(
89
+ 'Failed to display configuration interface' + error.message
90
+ )
91
+ }
92
+ }
93
+
94
+ private createHeader(): void {
95
+ this.containerEl.createEl('h2', {
96
+ text: 'Configuration',
97
+ cls: 'configuration-title',
98
+ })
99
+
100
+ this.containerEl.createEl('p', {
101
+ text: 'Configure your Strapi export settings and schema mappings.',
102
+ cls: 'configuration-description',
103
+ })
104
+ }
105
+
106
+ private addRouteSelector(): void {
107
+ const routeSetting = new Setting(this.containerEl)
108
+ .setName('Select Route')
109
+ .setDesc('Choose the route to configure')
110
+ .addDropdown(dropdown => {
111
+ this.components.routeSelector = dropdown
112
+ this.plugin.settings.routes.forEach(route => {
113
+ dropdown.addOption(route.id, route.name)
114
+ })
115
+ dropdown.setValue(this.currentRouteId)
116
+ dropdown.onChange(async value => {
117
+ this.currentRouteId = value
118
+ await this.updateConfigurationFields()
119
+ })
120
+ })
121
+
122
+ this.addRouteManagementButtons(routeSetting)
123
+ }
124
+
125
+ private addRouteManagementButtons(setting: Setting): void {
126
+ setting
127
+ .addButton(button =>
128
+ button
129
+ .setButtonText('New Route')
130
+ .setTooltip('Create a new route configuration')
131
+ .onClick(() => this.createNewRoute())
132
+ )
133
+ .addButton(button =>
134
+ button
135
+ .setButtonText('Delete')
136
+ .setTooltip('Delete current route')
137
+ .onClick(() => this.deleteCurrentRoute())
138
+ )
139
+ }
140
+
141
+ private async createNewRoute(): Promise<void> {
142
+ try {
143
+ const newRoute: RouteConfig = {
144
+ // Base properties
145
+ id: `route-${Date.now()}`,
146
+ name: 'New Route',
147
+
148
+ // Strapi configuration
149
+ schema: '',
150
+ schemaDescription: '',
151
+ generatedConfig: '',
152
+ contentType: 'articles', // Default content type
153
+ contentField: 'content',
154
+
155
+ // UI configuration
156
+ icon: 'file-text', // Default icon
157
+ description: 'New export route',
158
+ subtitle: '',
159
+
160
+ // Route settings
161
+ url: '',
162
+ enabled: true,
163
+ language: 'en',
164
+
165
+ // Mappings and instructions
166
+ fieldMappings: {},
167
+ additionalInstructions: '',
168
+ }
169
+
170
+ this.plugin.settings.routes.push(newRoute)
171
+ await this.plugin.saveSettings()
172
+ this.currentRouteId = newRoute.id
173
+ this.display()
174
+ new Notice('New route created successfully')
175
+ } catch (error) {
176
+ this.showError('Failed to create new route' + error.message)
177
+ }
178
+ }
179
+
180
+ private async deleteCurrentRoute(): Promise<void> {
181
+ try {
182
+ if (this.plugin.settings.routes.length <= 1) {
183
+ new Notice('Cannot delete the only route')
184
+ return
185
+ }
186
+
187
+ const routeIndex = this.plugin.settings.routes.findIndex(
188
+ route => route.id === this.currentRouteId
189
+ )
190
+
191
+ if (routeIndex !== -1) {
192
+ this.plugin.settings.routes.splice(routeIndex, 1)
193
+ await this.plugin.saveSettings()
194
+ this.currentRouteId = this.plugin.settings.routes[0].id
195
+ this.display()
196
+ new Notice('Route deleted successfully')
197
+ }
198
+ } catch (error) {
199
+ this.showError('Failed to delete route' + error.message)
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Updates schema input handler
205
+ */
206
+ private addSchemaConfigSection(): void {
207
+ try {
208
+ // Strapi Schema Input
209
+ new Setting(this.containerEl)
210
+ .setName('Strapi Schema')
211
+ .setDesc('Paste your complete Strapi schema JSON here')
212
+ .addTextArea(text => {
213
+ this.components.schemaInput = text
214
+ text.setValue(this.getCurrentRouteSchema()).onChange(async value => {
215
+ // Validate JSON format
216
+ JSON.parse(value)
217
+ await this.updateCurrentRouteConfig('schema', value)
218
+ })
219
+ text.inputEl.rows = 10
220
+ text.inputEl.cols = 50
221
+ })
222
+
223
+ // Schema Description Input
224
+ new Setting(this.containerEl)
225
+ .setName('Schema Description')
226
+ .setDesc('Provide descriptions for each field in the schema')
227
+ .addTextArea(text => {
228
+ this.components.schemaDescriptionInput = text
229
+ text
230
+ .setValue(this.getCurrentRouteSchemaDescription())
231
+ .onChange(async value => {
232
+ JSON.parse(value)
233
+ await this.updateCurrentRouteConfig('schemaDescription', value)
234
+ })
235
+ text.inputEl.rows = 10
236
+ text.inputEl.cols = 50
237
+ })
238
+ } catch (error) {
239
+ this.showError(
240
+ 'Failed to set up schema configuration section' + error.message
241
+ )
242
+ }
243
+ }
244
+
245
+ private addContentFieldSection(): void {
246
+ new Setting(this.containerEl)
247
+ .setName('Content Field Name')
248
+ .setDesc(
249
+ 'Enter the name of the field where the main article content should be inserted'
250
+ )
251
+ .addText(text => {
252
+ this.components.contentFieldInput = text
253
+ text
254
+ .setValue(this.getCurrentRouteContentField())
255
+ .setPlaceholder('content')
256
+ .onChange(async value => {
257
+ await this.updateCurrentRouteConfig('contentField', value)
258
+ })
259
+ })
260
+ }
261
+
262
+ private addLanguageSection(): void {
263
+ new Setting(this.containerEl)
264
+ .setName('Target Language')
265
+ .setDesc('Select the target language for the exported content')
266
+ .addDropdown(dropdown => {
267
+ this.components.languageDropdown = dropdown
268
+ const languages = this.getAvailableLanguages()
269
+ Object.entries(languages).forEach(([code, name]) => {
270
+ dropdown.addOption(code, name)
271
+ })
272
+ dropdown
273
+ .setValue(this.getCurrentRouteLanguage())
274
+ .onChange(async value => {
275
+ await this.updateCurrentRouteConfig('language', value)
276
+ })
277
+ })
278
+ }
279
+
280
+ private addAutoConfigSection(): void {
281
+ new Setting(this.containerEl)
282
+ .setName('Auto-Configure')
283
+ .setDesc('Automatically configure fields using OpenAI (Experimental)')
284
+ .addButton(button => {
285
+ button
286
+ .setButtonText('Generate Configuration')
287
+ .setCta()
288
+ .onClick(() => this.generateConfiguration())
289
+ })
290
+
291
+ new Setting(this.containerEl)
292
+ .setName('Generated Configuration')
293
+ .setDesc(
294
+ 'The generated configuration will appear here. You can edit it if needed.'
295
+ )
296
+ .addTextArea(text => {
297
+ this.components.configOutput = text
298
+ text
299
+ .setValue(this.getCurrentRouteGeneratedConfig())
300
+ .onChange(async value => {
301
+ await this.updateCurrentRouteConfig('generatedConfig', value)
302
+ })
303
+ text.inputEl.rows = 10
304
+ text.inputEl.cols = 50
305
+ })
306
+
307
+ new Setting(this.containerEl)
308
+ .setName('Apply Configuration')
309
+ .setDesc('Use this configuration for the plugin')
310
+ .addButton(button => {
311
+ button
312
+ .setButtonText('Apply')
313
+ .setCta()
314
+ .onClick(() => this.applyConfiguration())
315
+ })
316
+ }
317
+
318
+ private async generateConfiguration(): Promise<void> {
319
+ try {
320
+ const currentRoute = this.plugin.settings.routes.find(
321
+ route => route.id === this.currentRouteId
322
+ )
323
+
324
+ if (!currentRoute) {
325
+ throw new Error('Current route not found')
326
+ }
327
+
328
+ // Validate OpenAI API key
329
+ if (!this.plugin.settings.openaiApiKey) {
330
+ throw new Error(
331
+ 'OpenAI API key not configured. Please configure it in settings.'
332
+ )
333
+ }
334
+
335
+ // Get both schema and schema description
336
+ const schema = currentRoute.schema
337
+ const schemaDescription = currentRoute.schemaDescription
338
+
339
+ if (!schema || !schemaDescription) {
340
+ throw new Error('Both schema and schema description are required')
341
+ }
342
+
343
+ // Generate configuration using the new service
344
+ const config = await this.configGenerator.generateConfiguration({
345
+ schema,
346
+ schemaDescription,
347
+ language: currentRoute.language,
348
+ additionalInstructions: currentRoute.additionalInstructions,
349
+ })
350
+
351
+ if (this.components.configOutput) {
352
+ // Format the output configuration
353
+ const formattedConfig = JSON.stringify(config, null, 2)
354
+
355
+ // Update the UI and save
356
+ await this.updateCurrentRouteConfig('generatedConfig', formattedConfig)
357
+
358
+ this.components.configOutput.setValue(formattedConfig)
359
+
360
+ new Notice('Configuration generated successfully!')
361
+ }
362
+ } catch (error) {
363
+ let errorMessage = 'Failed to generate configuration'
364
+
365
+ if (error instanceof SyntaxError) {
366
+ errorMessage = 'Invalid JSON format in schema or description'
367
+ } else if (error instanceof Error) {
368
+ errorMessage = error.message
369
+ }
370
+
371
+ new Notice(errorMessage)
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Apply the generated configuration
377
+ */
378
+ private async applyConfiguration(): Promise<void> {
379
+ try {
380
+ if (!this.components.configOutput) {
381
+ throw new Error('Configuration output not initialized')
382
+ }
383
+
384
+ const configValue = this.components.configOutput.getValue()
385
+
386
+ // Validate configuration format
387
+ const config = JSON.parse(configValue)
388
+
389
+ const currentRoute = this.plugin.settings.routes.find(
390
+ route => route.id === this.currentRouteId
391
+ )
392
+
393
+ if (!currentRoute) {
394
+ throw new Error('Current route not found')
395
+ }
396
+
397
+ // Update route with new configuration
398
+ currentRoute.fieldMappings = config.fieldMappings || {}
399
+ currentRoute.additionalInstructions = config.additionalInstructions || ''
400
+ currentRoute.contentField = config.contentField || 'content'
401
+
402
+ await this.plugin.saveSettings()
403
+ new Notice('Configuration applied successfully!')
404
+ } catch (error) {
405
+ new Notice(`Failed to apply configuration: ${error.message}`)
406
+ }
407
+ }
408
+
409
+ // Utility methods
410
+ private getAvailableLanguages(): Record<string, string> {
411
+ return {
412
+ en: 'English',
413
+ fr: 'French',
414
+ es: 'Spanish',
415
+ de: 'German',
416
+ it: 'Italian',
417
+ zh: 'Chinese',
418
+ ja: 'Japanese',
419
+ ko: 'Korean',
420
+ pt: 'Portuguese',
421
+ ru: 'Russian',
422
+ ar: 'Arabic',
423
+ hi: 'Hindi',
424
+ }
425
+ }
426
+
427
+ private showError(message: string): void {
428
+ new Notice(`Configuration Error: ${message}`)
429
+ }
430
+
431
+ private getCurrentRouteGeneratedConfig(): string {
432
+ return this.getCurrentRouteConfig('generatedConfig')
433
+ }
434
+
435
+ private getCurrentRouteSchema(): string {
436
+ return this.getCurrentRouteConfig('schema')
437
+ }
438
+
439
+ private getCurrentRouteSchemaDescription(): string {
440
+ return this.getCurrentRouteConfig('schemaDescription')
441
+ }
442
+
443
+ private getCurrentRouteLanguage(): string {
444
+ return this.getCurrentRouteConfig('language') || 'en'
445
+ }
446
+
447
+ private getCurrentRouteContentField(): string {
448
+ return this.getCurrentRouteConfig('contentField') || 'content'
449
+ }
450
+
451
+ private getCurrentRouteConfig(key: string): string {
452
+ const currentRoute = this.plugin.settings.routes.find(
453
+ route => route.id === this.currentRouteId
454
+ )
455
+ return currentRoute?.[key] || ''
456
+ }
457
+
458
+ private async updateCurrentRouteConfig(
459
+ key: string,
460
+ value: string
461
+ ): Promise<void> {
462
+ const routeIndex = this.plugin.settings.routes.findIndex(
463
+ route => route.id === this.currentRouteId
464
+ )
465
+ if (routeIndex !== -1) {
466
+ this.plugin.settings.routes[routeIndex][key] = value
467
+ await this.plugin.saveSettings()
468
+ }
469
+ }
470
+
471
+ private async updateConfigurationFields(): Promise<void> {
472
+ try {
473
+ const currentRoute = this.plugin.settings.routes.find(
474
+ route => route.id === this.currentRouteId
475
+ )
476
+ if (currentRoute) {
477
+ if (this.components.schemaInput) {
478
+ this.components.schemaInput.setValue(currentRoute.schema || '')
479
+ }
480
+ if (this.components.schemaDescriptionInput) {
481
+ this.components.schemaDescriptionInput.setValue(
482
+ currentRoute.schemaDescription || ''
483
+ )
484
+ }
485
+ if (this.components.configOutput) {
486
+ this.components.configOutput.setValue(
487
+ currentRoute.generatedConfig || ''
488
+ )
489
+ }
490
+ if (this.components.languageDropdown) {
491
+ this.components.languageDropdown.setValue(
492
+ currentRoute.language || 'en'
493
+ )
494
+ }
495
+ }
496
+ } catch (error) {
497
+ this.showError('Failed to update configuration fields' + error.message)
498
+ }
499
+ }
500
+
501
+ private async identifyImageFields(
502
+ generatedConfig: string
503
+ ): Promise<string[]> {
504
+ try {
505
+ const analysis = await this.fieldAnalyzer.analyzeSchema(generatedConfig)
506
+ return analysis.imageFields.map(field => field.fieldName)
507
+ } catch (error) {
508
+ new Notice(
509
+ 'Error identifying image fields. Please try again.' + error.message
510
+ )
511
+ return []
512
+ }
513
+ }
514
+
515
+ private async openImageSelectionModal(
516
+ imageFields: string[],
517
+ currentRoute: RouteConfig
518
+ ): Promise<void> {
519
+ const modal = new ImageSelectionModal(
520
+ this.app,
521
+ imageFields,
522
+ async selections => {
523
+ await this.handleImageSelections(selections, currentRoute)
524
+ }
525
+ )
526
+ modal.open()
527
+ }
528
+
529
+ private async handleImageSelections(
530
+ selections: Record<string, string>,
531
+ currentRoute: RouteConfig
532
+ ): Promise<void> {
533
+ try {
534
+ for (const [field, imagePath] of Object.entries(selections)) {
535
+ if (imagePath) {
536
+ await this.processImageField(field, imagePath, currentRoute)
537
+ }
538
+ }
539
+ await this.plugin.saveSettings()
540
+ new Notice('Image configurations updated successfully')
541
+ } catch (error) {
542
+ this.showError('Failed to process image selections' + error.message)
543
+ }
544
+ }
545
+
546
+ private async processImageField(
547
+ field: string,
548
+ imagePath: string,
549
+ currentRoute: RouteConfig
550
+ ): Promise<void> {
551
+ const file = this.app.vault.getAbstractFileByPath(imagePath)
552
+ if (!(file instanceof TFile)) {
553
+ throw new Error(`File not found: ${imagePath}`)
554
+ }
555
+
556
+ const result = await uploadImageToStrapi(
557
+ file,
558
+ file.name,
559
+ this.plugin.settings,
560
+ this.app
561
+ )
562
+
563
+ if (result?.url) {
564
+ // Initialiser le fieldMapping avec les propriétés requises
565
+ if (!currentRoute.fieldMappings[field]) {
566
+ currentRoute.fieldMappings[field] = {
567
+ obsidianSource: 'frontmatter', // ou 'content' selon le cas
568
+ type: 'string',
569
+ format: 'url',
570
+ required: false,
571
+ transform: 'value => value', // transformation par défaut
572
+ validation: {
573
+ type: 'string',
574
+ pattern: '^https?://.+',
575
+ },
576
+ }
577
+ }
578
+
579
+ currentRoute.fieldMappings[field].value = result.url
580
+ } else {
581
+ throw new Error('Failed to upload image')
582
+ }
583
+ }
584
+ }
585
+
586
+ class ImageSelectionModal extends Modal {
587
+ private imageFields: string[]
588
+ private onSubmit: (selections: Record<string, string>) => void
589
+ private selections: Record<string, string> = {}
590
+
591
+ constructor(
592
+ app: App,
593
+ imageFields: string[],
594
+ onSubmit: (selections: Record<string, string>) => void
595
+ ) {
596
+ super(app)
597
+ this.imageFields = imageFields
598
+ this.onSubmit = onSubmit
599
+ }
600
+
601
+ onOpen(): void {
602
+ const { contentEl } = this
603
+
604
+ contentEl.createEl('h2', { text: 'Select Images for Fields' })
605
+
606
+ this.imageFields.forEach(field => {
607
+ this.createFieldSelector(contentEl, field)
608
+ })
609
+
610
+ this.createButtons(contentEl)
611
+ }
612
+
613
+ private createFieldSelector(container: HTMLElement, field: string): Setting {
614
+ const setting = new Setting(container)
615
+ .setName(field)
616
+ .setDesc(`Select an image for ${field}`)
617
+ .addButton(button =>
618
+ button.setButtonText('Choose Image').onClick(async () => {
619
+ const imagePath = await this.selectImage()
620
+ if (imagePath) {
621
+ this.selections[field] = imagePath
622
+ button.setButtonText('Change Image')
623
+ new Notice(`Image selected for ${field}`)
624
+ }
625
+ })
626
+ )
627
+
628
+ return setting
629
+ }
630
+
631
+ private createButtons(container: HTMLElement): void {
632
+ const buttonContainer = container.createDiv('modal-button-container')
633
+
634
+ new Setting(buttonContainer)
635
+ .addButton(button =>
636
+ button
637
+ .setButtonText('Confirm')
638
+ .setCta()
639
+ .onClick(() => {
640
+ this.close()
641
+ this.onSubmit(this.selections)
642
+ })
643
+ )
644
+ .addButton(button =>
645
+ button.setButtonText('Cancel').onClick(() => {
646
+ this.close()
647
+ })
648
+ )
649
+ }
650
+
651
+ private async selectImage(): Promise<string | null> {
652
+ // TODO: Implement file selection using Obsidian's API
653
+ // This is a placeholder that should be replaced with actual file selection logic
654
+ return 'path/to/image.jpg'
655
+ }
656
+
657
+ onClose(): void {
658
+ const { contentEl } = this
659
+ contentEl.empty()
660
+ }
661
+ }
662
+
663
+ export default Configuration