notes-to-strapi-export-article-ai 1.0.118 → 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 -118
package/src/main.ts CHANGED
@@ -1,44 +1,89 @@
1
- import { Plugin } from 'obsidian'
2
- import { StrapiExporterSettingTab } from './settings'
1
+ import { Notice, Plugin } from 'obsidian'
3
2
  import { DEFAULT_STRAPI_EXPORTER_SETTINGS } from './constants'
4
- import { processMarkdownContent } from './utils/image-processor'
5
- import { StrapiExporterSettings } from './types/settings'
3
+ import { RouteConfig, StrapiExporterSettings, AnalyzedContent } from './types'
4
+ import { UnifiedSettingsTab } from './settings/UnifiedSettingsTab'
5
+ import { debounce } from './utils/debounce'
6
+ import { analyzeFile } from './utils/analyse-file'
7
+ import { showPreviewToUser } from './utils/preview-modal'
8
+ import { processImages } from './utils/process-file'
9
+ import { StrapiExportService } from './services/strapi-export'
6
10
 
7
11
  export default class StrapiExporterPlugin extends Plugin {
8
12
  settings: StrapiExporterSettings
13
+ ribbonIcons: Map<string, HTMLElement> = new Map()
14
+ settingsTab: UnifiedSettingsTab
15
+ debouncedUpdateRibbonIcons: () => Promise<void>
9
16
 
10
- async onload() {
11
- await this.loadSettings()
12
-
13
- // Add ribbon icons and event listeners
14
- /**
15
- * Add a ribbon icon to the Markdown view (the little icon on the left side bar)
16
- */
17
- this.addRibbonIcon(
18
- 'upload',
19
- 'Upload to Strapi and generate content with AI',
20
- async () => {
21
- await this.processMarkdownContent()
17
+ private loadStyles(): void {
18
+ const styleElement = document.createElement('style')
19
+ styleElement.id = 'strapi-exporter-styles'
20
+ document.head.appendChild(styleElement)
21
+
22
+ styleElement.textContent = `
23
+ .strapi-exporter-nav {
24
+ display: flex;
25
+ justify-content: center;
26
+ gap: 20px;
27
+ margin-bottom: 20px;
22
28
  }
23
- )
24
29
 
25
- /**
26
- * Add a ribbon icon based on the settings (if enabled)
27
- */
28
- if (this.settings.enableAdditionalApiCall) {
29
- this.addRibbonIcon(
30
- 'link',
31
- 'Upload to Strapi and generate additional content with AI',
32
- async () => {
33
- await this.processMarkdownContent(true)
34
- }
30
+ .strapi-exporter-nav-button {
31
+ padding: 10px 15px;
32
+ border: none;
33
+ background: none;
34
+ cursor: pointer;
35
+ font-size: 14px;
36
+ color: var(--text-muted);
37
+ transition: all 0.3s ease;
38
+ }
39
+
40
+ .strapi-exporter-nav-button:hover {
41
+ color: var(--text-normal);
42
+ }
43
+
44
+ .strapi-exporter-nav-button.is-active {
45
+ color: var(--text-accent);
46
+ border-bottom: 2px solid var(--text-accent);
47
+ }
48
+
49
+ .strapi-exporter-content {
50
+ padding: 20px;
51
+ }
52
+ `
53
+ }
54
+
55
+ async onload() {
56
+ try {
57
+ // Loading settings
58
+ await this.loadSettings()
59
+
60
+ // Loading styles
61
+ this.loadStyles()
62
+
63
+ // Configuring icon updates
64
+ this.debouncedUpdateRibbonIcons = debounce(
65
+ this.updateRibbonIcons.bind(this),
66
+ 300
35
67
  )
68
+
69
+ // Setting up UI
70
+ this.settingsTab = new UnifiedSettingsTab(this.app, this)
71
+ this.addSettingTab(this.settingsTab)
72
+
73
+ // Updating icons
74
+ await this.updateRibbonIcons()
75
+ } catch (error) {
76
+ new Notice('Error loading Strapi Exporter plugin' + error.message)
36
77
  }
78
+ }
37
79
 
38
- this.addSettingTab(new StrapiExporterSettingTab(this.app, this))
80
+ async saveSettings() {
81
+ await this.saveData(this.settings)
39
82
  }
40
83
 
41
- onunload() {}
84
+ onunload() {
85
+ this.removeAllIcons()
86
+ }
42
87
 
43
88
  async loadSettings() {
44
89
  this.settings = Object.assign(
@@ -48,12 +93,110 @@ export default class StrapiExporterPlugin extends Plugin {
48
93
  )
49
94
  }
50
95
 
51
- async saveSettings() {
52
- await this.saveData(this.settings)
96
+ updateRibbonIcons() {
97
+ this.removeAllIcons()
98
+
99
+ this.settings.routes.forEach(route => {
100
+ if (route.enabled) {
101
+ this.addIconForRoute(route)
102
+ }
103
+ })
104
+ }
105
+
106
+ removeAllIcons() {
107
+ this.ribbonIcons.forEach((icon, routeId) => {
108
+ if (icon && icon.parentNode) {
109
+ icon.parentNode.removeChild(icon)
110
+ }
111
+ })
112
+ this.ribbonIcons.clear()
53
113
  }
54
114
 
55
- async processMarkdownContent(useAdditionalCallAPI = false) {
56
- // Call processMarkdownContent from image-processor.ts
57
- await processMarkdownContent(this.app, this.settings, useAdditionalCallAPI)
115
+ addIconForRoute(route: RouteConfig) {
116
+ const existingIcon = this.ribbonIcons.get(route.id)
117
+ if (existingIcon && existingIcon.parentNode) {
118
+ existingIcon.parentNode.removeChild(existingIcon)
119
+ }
120
+
121
+ const ribbonIconEl = this.addRibbonIcon(route.icon, route.name, () => {
122
+ this.exportToStrapi(route.id)
123
+ })
124
+ this.ribbonIcons.set(route.id, ribbonIconEl)
125
+ }
126
+
127
+ async exportToStrapi(routeId: string) {
128
+ // Route validation
129
+ const route = this.settings.routes.find(r => r.id === routeId)
130
+ if (!route) {
131
+ new Notice('Export failed: Route not found')
132
+ return
133
+ }
134
+
135
+ // Active file check
136
+ const activeFile = this.app.workspace.getActiveFile()
137
+ if (!activeFile) {
138
+ new Notice('No active file')
139
+ return
140
+ }
141
+
142
+ try {
143
+ // File analysis
144
+ const analyzedContent = await analyzeFile(activeFile, this.app, route)
145
+
146
+ // Image processing
147
+ const processedContent = await processImages(
148
+ analyzedContent,
149
+ this.app,
150
+ this.settings
151
+ )
152
+
153
+ // User preview
154
+ const userConfirmed = await showPreviewToUser(
155
+ this.app,
156
+ processedContent,
157
+ this
158
+ )
159
+
160
+ if (!userConfirmed) {
161
+ new Notice('Export cancelled by user')
162
+ return
163
+ }
164
+
165
+ // Initialize Strapi export service
166
+ const strapiExport = new StrapiExportService(
167
+ this.settings,
168
+ this.app,
169
+ activeFile
170
+ )
171
+
172
+ // Export to Strapi
173
+ await strapiExport.exportContent(processedContent, route)
174
+
175
+ new Notice('Content successfully exported to Strapi!')
176
+ } catch (error) {
177
+ new Notice(`Export failed: ${error.message}`)
178
+ }
179
+ }
180
+
181
+ async sendToStrapi(content: AnalyzedContent, route: RouteConfig) {
182
+ const response = await fetch(
183
+ `${this.settings.strapiUrl}/api/${route.contentType}`,
184
+ {
185
+ method: 'POST',
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ Authorization: `Bearer ${this.settings.strapiApiToken}`,
189
+ },
190
+ body: JSON.stringify({ data: content }),
191
+ }
192
+ )
193
+
194
+ if (!response.ok) {
195
+ const errorData = await response.json()
196
+ throw new Error(
197
+ errorData.error.message || 'Failed to send content to Strapi'
198
+ )
199
+ }
200
+ new Notice('Content successfully sent to Strapi!')
58
201
  }
59
202
  }
@@ -0,0 +1,172 @@
1
+ import { createOpenAI } from '@ai-sdk/openai'
2
+ import { generateObject } from 'ai'
3
+
4
+ export class ConfigurationGenerator {
5
+ private model
6
+
7
+ constructor(options: { openaiApiKey: string }) {
8
+ const openai = createOpenAI({
9
+ apiKey: options.openaiApiKey,
10
+ })
11
+
12
+ this.model = openai('gpt-4o-mini')
13
+ }
14
+
15
+ async generateConfiguration(params: {
16
+ schema: string
17
+ schemaDescription: string
18
+ language: string
19
+ additionalInstructions?: string
20
+ }) {
21
+ // Parse input schemas
22
+ const schema = JSON.parse(params.schema)
23
+ const descriptions = JSON.parse(params.schemaDescription)
24
+
25
+ // Generate field configurations
26
+ const { object } = await generateObject({
27
+ model: this.model,
28
+ output: 'no-schema',
29
+ prompt: this.buildPrompt(schema.data, descriptions.data, params.language),
30
+ })
31
+
32
+ // Transform to final configuration
33
+ return this.transformToConfiguration(object)
34
+ }
35
+
36
+ private buildPrompt(
37
+ schema: Record<string, any>,
38
+ descriptions: Record<string, any>,
39
+ language: string
40
+ ): string {
41
+ return `Analyze this Strapi schema and create field configurations:
42
+
43
+ SCHEMA:
44
+ ${JSON.stringify(schema, null, 2)}
45
+
46
+ FIELD DESCRIPTIONS:
47
+ ${JSON.stringify(descriptions, null, 2)}
48
+
49
+ Create a configuration where each field has:
50
+ 1. type:
51
+ - "string" for text fields
52
+ - "media" for image fields (when type is "string or id")
53
+ - "array" for lists
54
+ - "object" for complex fields
55
+ - "number" for numeric fields
56
+
57
+ 2. source:
58
+ - "content" for the main content field
59
+ - "frontmatter" for frontmatter fields
60
+
61
+ 3. description:
62
+ - Use the provided descriptions
63
+ - Keep descriptions in ${language}
64
+ - Make them clear and concise
65
+
66
+ 4. required:
67
+ - true for essential fields (title, content)
68
+ - false for optional fields
69
+
70
+ 5. format (when applicable):
71
+ - "url" for media fields
72
+ - "slug" for URL-friendly fields
73
+
74
+ Example field configuration:
75
+ {
76
+ "title": {
77
+ "type": "string",
78
+ "description": "The main title of the article",
79
+ "required": true,
80
+ "source": "frontmatter"
81
+ },
82
+ "image_presentation": {
83
+ "type": "media",
84
+ "description": "Main article image",
85
+ "required": true,
86
+ "source": "frontmatter",
87
+ "format": "url"
88
+ },
89
+ "content": {
90
+ "type": "string",
91
+ "description": "The main content of the article",
92
+ "required": true,
93
+ "source": "content"
94
+ }
95
+ }
96
+
97
+ Generate field configurations maintaining the original schema structure.`
98
+ }
99
+
100
+ private transformToConfiguration(generated: any) {
101
+ // Transform the output to the expected format
102
+ let fieldMappings = {}
103
+ if (!generated.fields) {
104
+ fieldMappings = Object.entries(generated).reduce(
105
+ (acc, [key, field]: [string, any]) => ({
106
+ ...acc,
107
+ [key]: {
108
+ obsidianSource: field.source,
109
+ type: field.type,
110
+ description: field.description,
111
+ required: field.required,
112
+ ...(field.format && { format: field.format }),
113
+ ...(field.type === 'media' && {
114
+ validation: {
115
+ type: 'string',
116
+ pattern: '^https?://.+',
117
+ },
118
+ }),
119
+ ...(field.type === 'array' && {
120
+ transform: this.getArrayTransform(key),
121
+ }),
122
+ },
123
+ }),
124
+ {}
125
+ )
126
+ } else {
127
+ fieldMappings = Object.entries(generated.fields).reduce(
128
+ (acc, [key, field]: [string, any]) => ({
129
+ ...acc,
130
+ [key]: {
131
+ obsidianSource: field.source,
132
+ type: field.type,
133
+ description: field.description,
134
+ required: field.required,
135
+ ...(field.format && { format: field.format }),
136
+ ...(field.type === 'media' && {
137
+ validation: {
138
+ type: 'string',
139
+ pattern: '^https?://.+',
140
+ },
141
+ }),
142
+ ...(field.type === 'array' && {
143
+ transform: this.getArrayTransform(key),
144
+ }),
145
+ },
146
+ }),
147
+ {}
148
+ )
149
+ }
150
+
151
+ return {
152
+ fieldMappings,
153
+ contentField: 'content',
154
+ }
155
+ }
156
+
157
+ private getArrayTransform(fieldName: string): string {
158
+ const transforms = {
159
+ gallery:
160
+ 'value => Array.isArray(value) ? value : value.split(",").map(url => url.trim())',
161
+ tags: 'value => Array.isArray(value) ? value : value.split(",").map(tag => ({ name: tag.trim() }))',
162
+ links: `value => Array.isArray(value) ? value : value.split(";").map(link => {
163
+ const [label, url] = link.split("|").map(s => s.trim());
164
+ return { label, url };
165
+ })`,
166
+ }
167
+
168
+ return (
169
+ transforms[fieldName] || 'value => Array.isArray(value) ? value : [value]'
170
+ )
171
+ }
172
+ }
@@ -0,0 +1,84 @@
1
+ import { z } from 'zod'
2
+ import { generateObject } from 'ai'
3
+ import { createOpenAI } from '@ai-sdk/openai'
4
+
5
+ /**
6
+ * Schema for field analysis result
7
+ */
8
+ const fieldAnalysisSchema = z.object({
9
+ imageFields: z.array(
10
+ z.object({
11
+ fieldName: z.string(),
12
+ fieldType: z.enum(['single-image', 'gallery', 'other']),
13
+ required: z.boolean(),
14
+ description: z.string(),
15
+ })
16
+ ),
17
+ metadataFields: z.array(
18
+ z.object({
19
+ fieldName: z.string(),
20
+ valueType: z.string(),
21
+ description: z.string(),
22
+ })
23
+ ),
24
+ contentFields: z.array(
25
+ z.object({
26
+ fieldName: z.string(),
27
+ contentType: z.string(),
28
+ format: z.string().optional(),
29
+ })
30
+ ),
31
+ })
32
+
33
+ type FieldAnalysis = z.infer<typeof fieldAnalysisSchema>
34
+
35
+ export interface FieldAnalyzerOptions {
36
+ openaiApiKey: string
37
+ }
38
+
39
+ export class StructuredFieldAnalyzer {
40
+ private model
41
+
42
+ constructor(options: FieldAnalyzerOptions) {
43
+ const openai = createOpenAI({
44
+ apiKey: options.openaiApiKey,
45
+ })
46
+ this.model = openai('gpt-4o-mini', {
47
+ structuredOutputs: true,
48
+ })
49
+ }
50
+
51
+ /**
52
+ * Analyze JSON schema to identify field types and purposes
53
+ */
54
+ async analyzeSchema(schema: string): Promise<FieldAnalysis> {
55
+ try {
56
+ const { object } = await generateObject({
57
+ model: this.model,
58
+ schema: fieldAnalysisSchema,
59
+ schemaName: 'SchemaAnalysis',
60
+ schemaDescription:
61
+ 'Analysis of content schema fields to identify types and purposes',
62
+ prompt: this.buildAnalysisPrompt(schema),
63
+ })
64
+
65
+ return object as FieldAnalysis
66
+ } catch (error) {
67
+ throw new Error(`Schema analysis failed: ${error.message}`)
68
+ }
69
+ }
70
+
71
+ private buildAnalysisPrompt(schema: string): string {
72
+ return `Analyze the following JSON schema and identify:
73
+ - Image fields (single images and galleries)
74
+ - Metadata fields (SEO, tags, dates, etc.)
75
+ - Content fields (text, rich text, markdown)
76
+
77
+ Provide structured categorization of all fields with their purposes and data types.
78
+
79
+ Schema to analyze:
80
+ ${schema}
81
+
82
+ Focus on identifying field types, required status, and provide clear descriptions.`
83
+ }
84
+ }