notes-to-strapi-export-article-ai 1.0.119 → 3.0.2
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/.eslintrc +30 -22
- package/README.md +98 -143
- package/images/img.png +0 -0
- package/images/img_1.png +0 -0
- package/images/img_10.png +0 -0
- package/images/img_11.png +0 -0
- package/images/img_12.png +0 -0
- package/images/img_13.png +0 -0
- package/images/img_2.png +0 -0
- package/images/img_3.png +0 -0
- package/images/img_4.png +0 -0
- package/images/img_5.png +0 -0
- package/images/img_6.png +0 -0
- package/images/img_7.png +0 -0
- package/images/img_8.png +0 -0
- package/images/img_9.png +0 -0
- package/manifest.json +2 -2
- package/package.json +29 -26
- package/src/components/APIKeys.ts +219 -0
- package/src/components/Configuration.ts +663 -0
- package/src/components/Dashboard.ts +184 -0
- package/src/components/ImageSelectionModal.ts +58 -0
- package/src/components/Routes.ts +279 -0
- package/src/constants.ts +22 -61
- package/src/main.ts +177 -34
- package/src/services/configuration-generator.ts +172 -0
- package/src/services/field-analyzer.ts +84 -0
- package/src/services/frontmatter.ts +329 -0
- package/src/services/strapi-export.ts +436 -0
- package/src/settings/UnifiedSettingsTab.ts +206 -0
- package/src/types/image.ts +27 -16
- package/src/types/index.ts +3 -0
- package/src/types/route.ts +51 -0
- package/src/types/settings.ts +22 -23
- package/src/utils/analyse-file.ts +94 -0
- package/src/utils/debounce.ts +34 -0
- package/src/utils/image-processor.ts +124 -400
- package/src/utils/preview-modal.ts +265 -0
- package/src/utils/process-file.ts +122 -0
- package/src/utils/strapi-uploader.ts +120 -119
- package/src/settings.ts +0 -404
- package/src/types/article.ts +0 -8
- package/src/utils/openai-generator.ts +0 -139
- package/src/utils/validators.ts +0 -8
- package/version-bump.mjs +0 -14
- package/versions.json +0 -119
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|