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,329 @@
1
+ import { generateText } from 'ai'
2
+ import { createOpenAI } from '@ai-sdk/openai'
3
+ import { App, TFile } from 'obsidian'
4
+ import { RouteConfig } from '../types'
5
+ import StrapiExporterPlugin from '../main'
6
+
7
+ /**
8
+ * Interface defining the structure of a field in the schema
9
+ */
10
+ interface FieldMapping {
11
+ obsidianSource: 'frontmatter' | 'content'
12
+ type: string
13
+ description: string
14
+ required: boolean
15
+ format?: string
16
+ transform?: string
17
+ validation?: {
18
+ type: string
19
+ pattern?: string
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Interface for the complete configuration of field mappings
25
+ */
26
+ interface GeneratedConfig {
27
+ fieldMappings: Record<string, FieldMapping>
28
+ contentField: string
29
+ }
30
+
31
+ /**
32
+ * FrontmatterGenerator class handles the generation of YAML frontmatter
33
+ * based on a dynamic schema configuration from Strapi
34
+ */
35
+ export class FrontmatterGenerator {
36
+ private model
37
+ private plugin: StrapiExporterPlugin
38
+
39
+ /**
40
+ * Initialize the generator with the plugin instance
41
+ * @param plugin - The Strapi Exporter plugin instance
42
+ */
43
+ constructor(plugin: StrapiExporterPlugin) {
44
+ this.plugin = plugin
45
+ const openai = createOpenAI({
46
+ apiKey: this.plugin.settings.openaiApiKey,
47
+ })
48
+ this.model = openai('gpt-4o-mini')
49
+ }
50
+
51
+ /**
52
+ * Updates or creates frontmatter in an existing file
53
+ * @param file - The file to update
54
+ * @param app - The Obsidian app instance
55
+ * @returns Promise<string> Updated content with new frontmatter
56
+ */
57
+ async updateContentFrontmatter(file: TFile, app: App): Promise<string> {
58
+ const content = await app.vault.read(file)
59
+ const newFrontmatter = await this.generateFrontmatter(file, app)
60
+ const updatedContent = this.replaceFrontmatter(content, newFrontmatter)
61
+
62
+ return updatedContent
63
+ }
64
+
65
+ /**
66
+ * Generates structured example data based on field type and format
67
+ * @param field - The field mapping configuration
68
+ * @returns An appropriate example value for the field type
69
+ */
70
+ private generateExampleValue(field: FieldMapping): any {
71
+ const { type, format } = field
72
+
73
+ // Handle arrays with specific structures
74
+ if (type === 'array') {
75
+ return this.generateArrayExample(field)
76
+ }
77
+
78
+ // Handle media fields
79
+ if (type === 'media') {
80
+ return 'https://example.com/image.jpg'
81
+ }
82
+
83
+ // Handle standard types with formats
84
+ switch (type) {
85
+ case 'string':
86
+ if (format === 'url') return 'https://example.com'
87
+ if (format === 'slug') return 'example-slug'
88
+ return 'Example text'
89
+ case 'number':
90
+ return 1
91
+ case 'boolean':
92
+ return true
93
+ default:
94
+ return 'Example value'
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Generates example array content based on the field's transform configuration
100
+ * @param field - The field mapping configuration
101
+ * @returns An example array structure
102
+ */
103
+ private generateArrayExample(field: FieldMapping): any {
104
+ if (!field.transform) {
105
+ return ['example1', 'example2']
106
+ }
107
+
108
+ // Analyze transform string to determine array structure
109
+ const transform = field.transform
110
+
111
+ // Handle different array structures based on transform content
112
+ if (transform.includes('name:')) {
113
+ return [
114
+ { name: 'example1', id: 1 },
115
+ { name: 'example2', id: 2 },
116
+ ]
117
+ }
118
+
119
+ if (transform.includes('label:') && transform.includes('url:')) {
120
+ return [
121
+ { label: 'Example Link', url: 'https://example.com' },
122
+ { label: 'Another Link', url: 'https://example.com/page' },
123
+ ]
124
+ }
125
+
126
+ return ['example1', 'example2']
127
+ }
128
+
129
+ /**
130
+ * Builds a detailed prompt for GPT based on the schema configuration
131
+ * @param content - The content to analyze
132
+ * @param config - The generated configuration
133
+ * @returns A structured prompt string
134
+ */
135
+ private buildPrompt(content: string, config: GeneratedConfig): string {
136
+ const exampleData = {}
137
+ for (const [field, mapping] of Object.entries(config.fieldMappings)) {
138
+ exampleData[field] = this.generateExampleValue(mapping)
139
+ }
140
+
141
+ const exampleYaml = this.convertToYaml(exampleData)
142
+ const requiredFields = Object.entries(config.fieldMappings)
143
+ .filter(([_, mapping]) => mapping.required)
144
+ .map(([field]) => field)
145
+ .join(', ')
146
+
147
+ const prompt = `Generate YAML frontmatter that follows this exact schema and format:
148
+
149
+ Schema Definition:
150
+ ${JSON.stringify(config.fieldMappings, null, 2)}
151
+
152
+ Expected Format Example:
153
+ ---
154
+ ${exampleYaml}---
155
+
156
+ Requirements:
157
+ 1. Maintain exact field names as shown in the schema
158
+ 2. Follow YAML formatting and indentation precisely
159
+ 3. Generate appropriate content for each field type
160
+ 4. Preserve array structures as shown in the example
161
+ 5. Required fields: ${requiredFields}
162
+ 6. Use quotes for string values
163
+ 7. Maintain proper data types (strings, numbers, arrays)
164
+
165
+ Content for Analysis:
166
+ ${content.substring(0, 2000)}
167
+
168
+ DO NOT include the content IN the generated frontmatter. Just use the content to generate the frontmatter as context.
169
+ the content field is ${config.contentField}. Please delete the content field from the frontmatter.
170
+
171
+ Return complete YAML frontmatter with opening and closing "---" markers.`
172
+
173
+ return prompt
174
+ }
175
+
176
+ /**
177
+ * Converts a JavaScript object to properly formatted YAML
178
+ * @param obj - The object to convert
179
+ * @param indent - Current indentation level
180
+ * @returns Formatted YAML string
181
+ */
182
+ private convertToYaml(obj: any, indent: number = 0): string {
183
+ const spaces = ' '.repeat(indent)
184
+ let yaml = ''
185
+
186
+ for (const [key, value] of Object.entries(obj)) {
187
+ if (Array.isArray(value)) {
188
+ yaml += `${spaces}${key}:\n`
189
+ value.forEach(item => {
190
+ if (typeof item === 'object') {
191
+ yaml += `${spaces} -\n`
192
+ Object.entries(item).forEach(([k, v]) => {
193
+ yaml += `${spaces} ${k}: "${v}"\n`
194
+ })
195
+ } else {
196
+ yaml += `${spaces} - "${item}"\n`
197
+ }
198
+ })
199
+ } else if (typeof value === 'object' && value !== null) {
200
+ yaml += `${spaces}${key}:\n`
201
+ yaml += this.convertToYaml(value, indent + 2)
202
+ } else {
203
+ const formattedValue = typeof value === 'string' ? `"${value}"` : value
204
+ yaml += `${spaces}${key}: ${formattedValue}\n`
205
+ }
206
+ }
207
+
208
+ return yaml
209
+ }
210
+
211
+ /**
212
+ * Replaces or adds frontmatter in content
213
+ * @param content - Original content
214
+ * @param newFrontmatter - New frontmatter to add
215
+ * @returns Updated content with new frontmatter
216
+ */
217
+ private replaceFrontmatter(content: string, newFrontmatter: string): string {
218
+ const hasFrontmatter = /^---\n[\s\S]*?\n---/.test(content)
219
+
220
+ if (hasFrontmatter) {
221
+ return content.replace(/^---\n[\s\S]*?\n---/, newFrontmatter.trim())
222
+ } else {
223
+ return `${newFrontmatter}${content}`
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Gets the currently active route from plugin settings
229
+ * @returns The active route configuration or undefined
230
+ */
231
+ private getCurrentRoute(): RouteConfig | undefined {
232
+ return this.plugin.settings.routes.find(route => route.enabled)
233
+ }
234
+
235
+ /**
236
+ * Main method to generate frontmatter based on file content and schema
237
+ * @param file - The file to process
238
+ * @param app - The Obsidian app instance
239
+ * @returns Promise<string> Generated frontmatter
240
+ */
241
+ async generateFrontmatter(file: TFile, app: App): Promise<string> {
242
+ const content = await app.vault.read(file)
243
+ const currentRoute = this.getCurrentRoute()
244
+
245
+ if (!currentRoute?.generatedConfig) {
246
+ throw new Error('Generated configuration not found')
247
+ }
248
+
249
+ // Parse configuration and generate frontmatter
250
+ const config: GeneratedConfig = JSON.parse(currentRoute.generatedConfig)
251
+
252
+ const { text } = await generateText({
253
+ model: this.model,
254
+ system:
255
+ 'You are an author writing frontmatter for a new document, we are using Obsidian, and we need the correct format for the data given by the user.' +
256
+ 'Think about the data structure and the fields that are required for the frontmatter. The data should be in YAML format and follow the schema provided by the user.' +
257
+ 'And think about SEO to include the right metadata for the document.',
258
+ prompt: this.buildPrompt(content, config),
259
+ })
260
+
261
+ const cleanedText = await this.formatAndValidateYAML(
262
+ this.cleanGPTOutput(text),
263
+ config,
264
+ currentRoute.language
265
+ )
266
+
267
+ return cleanedText
268
+ }
269
+
270
+ /**
271
+ * Cleans the GPT output by removing code block markers
272
+ * @param text - The raw GPT output
273
+ * @returns Cleaned YAML content
274
+ */
275
+ private cleanGPTOutput(text: string): string {
276
+ return text
277
+ .replace(/^```ya?ml\n?/i, '') // Remove starting ```yaml or ```yml
278
+ .replace(/```$/, '') // Remove ending ```
279
+ .trim() // Remove extra whitespace
280
+ }
281
+
282
+ private async formatAndValidateYAML(
283
+ initialText: string,
284
+ config: GeneratedConfig,
285
+ language: string
286
+ ): Promise<string> {
287
+ const formattingPrompt = `As a YAML formatting expert, review and clean this frontmatter content.
288
+ Here are the specific requirements:
289
+
290
+ 1. Format Validation:
291
+ - Remove any unnecessary blank lines
292
+ - Ensure consistent indentation (2 spaces)
293
+ - Maintain compact array formatting
294
+ - Verify all strings are properly quoted
295
+
296
+ 2. Content Validation:
297
+ - All URLs should be valid format
298
+ - Tags should be compact without empty lines
299
+ - Array structures should be consistent
300
+ - Remove any suspicious or placeholder content
301
+
302
+ 3. Structure Requirements:
303
+ - Keep fields in logical order
304
+ - Ensure all required fields are present
305
+ - Verify data types match schema
306
+ - Check language consistency
307
+
308
+ 4. Stringified Context from the user :
309
+ content field: ${JSON.stringify(config.contentField)}
310
+ field mapping: ${JSON.stringify(config.fieldMappings)}
311
+
312
+ Original frontmatter to clean:
313
+ ${initialText}
314
+
315
+ Return only the cleaned YAML frontmatter, maintaining exact and correct formatting. No explanation needed.
316
+ - Ensure locale matches the content language (if specified) "${language}"
317
+ - if the frontmatter is not in the correct language, please change it to "${language}"
318
+ - TRANSLATE EVERYTHING TO "${language}"`
319
+
320
+ const { text } = await generateText({
321
+ model: this.model,
322
+ system:
323
+ 'You are a YAML formatting expert. Your only task is to clean and validate YAML frontmatter. Return only the cleaned YAML with no additional comments or explanations.',
324
+ prompt: formattingPrompt,
325
+ })
326
+
327
+ return this.cleanGPTOutput(text)
328
+ }
329
+ }