@stellartech/image-style-widget-directus 1.0.1 → 1.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.
@@ -1,549 +0,0 @@
1
- import { useApi } from '@directus/extensions-sdk';
2
- import { ref } from 'vue';
3
-
4
- export interface Style {
5
- id: string;
6
- name: string;
7
- prompt: string;
8
- example_1: string | null;
9
- example_2: string | null;
10
- is_active: boolean;
11
- sort: number;
12
- }
13
-
14
- export interface RegenerateResponse {
15
- file_id: string;
16
- url: string;
17
- }
18
-
19
- export interface ContentImage {
20
- id: number;
21
- image: string | null; // file UUID
22
- prompt: string | null;
23
- status: string;
24
- }
25
-
26
- export interface ImageVariant {
27
- id: string;
28
- title: string; // placeholder/caption
29
- status: string;
30
- lesson?: string;
31
- images?: ContentImage[]; // related ContentImage items
32
- }
33
-
34
- export interface GeneratedImageSet {
35
- placeholder: string;
36
- prompt: string;
37
- image1: string | null;
38
- image2: string | null;
39
- selected: number | null;
40
- }
41
-
42
- // Response from the prompt generation service
43
- export interface ImagePrompt {
44
- placeholder: string;
45
- prompts: string[];
46
- }
47
-
48
- export interface PromptGenerationResponse {
49
- images: ImagePrompt[];
50
- }
51
-
52
- // Supported image generation models
53
- export type ImageModel = 'flux-kontext-pro' | 'gemini-2.5-flash-image';
54
-
55
- export const IMAGE_MODELS: { id: ImageModel; name: string }[] = [
56
- { id: 'flux-kontext-pro', name: 'Flux' },
57
- { id: 'gemini-2.5-flash-image', name: 'Nano Banana' },
58
- ];
59
-
60
- // Default service URL (used as fallback if WidgetConfig not set)
61
- const DEFAULT_SERVICE_URL = 'http://localhost:8004';
62
-
63
- export function useDirectusApi() {
64
- const api = useApi();
65
- const loading = ref(false);
66
- const error = ref<string | null>(null);
67
-
68
- /**
69
- * Fetch the service URL from WidgetConfig singleton collection
70
- */
71
- async function fetchServiceUrl(): Promise<string> {
72
- try {
73
- const response = await api.get('/items/WidgetConfig');
74
- return response.data.data?.service_url || DEFAULT_SERVICE_URL;
75
- } catch (e: any) {
76
- console.warn('Failed to fetch service URL, using default:', e.message);
77
- return DEFAULT_SERVICE_URL;
78
- }
79
- }
80
-
81
- /**
82
- * Update the service URL in WidgetConfig singleton collection
83
- */
84
- async function updateServiceUrl(url: string): Promise<boolean> {
85
- try {
86
- await api.patch('/items/WidgetConfig', {
87
- service_url: url,
88
- });
89
- return true;
90
- } catch (e: any) {
91
- error.value = e.message || 'Failed to update service URL';
92
- return false;
93
- }
94
- }
95
-
96
- /**
97
- * Check if the current user has admin role
98
- */
99
- async function checkIsAdmin(): Promise<boolean> {
100
- try {
101
- const response = await api.get('/users/me', {
102
- params: {
103
- fields: ['role.admin_access'],
104
- },
105
- });
106
- return response.data.data?.role?.admin_access === true;
107
- } catch (e: any) {
108
- console.warn('Failed to check admin status:', e.message);
109
- return false;
110
- }
111
- }
112
-
113
- /**
114
- * Fetch all active styles from the ImageStyles collection
115
- */
116
- async function fetchStyles(collection: string = 'ImageStyles'): Promise<Style[]> {
117
- loading.value = true;
118
- error.value = null;
119
-
120
- try {
121
- const response = await api.get(`/items/${collection}`, {
122
- params: {
123
- filter: { is_active: { _eq: true } },
124
- sort: ['sort'],
125
- fields: ['id', 'name', 'prompt', 'example_1', 'example_2', 'is_active', 'sort'],
126
- },
127
- });
128
- return response.data.data || [];
129
- } catch (e: any) {
130
- error.value = e.message || 'Failed to fetch styles';
131
- return [];
132
- } finally {
133
- loading.value = false;
134
- }
135
- }
136
-
137
- /**
138
- * Update a style's prompt
139
- */
140
- async function updateStylePrompt(collection: string, styleId: string, newPrompt: string): Promise<boolean> {
141
- loading.value = true;
142
- error.value = null;
143
-
144
- try {
145
- await api.patch(`/items/${collection}/${styleId}`, {
146
- prompt: newPrompt,
147
- });
148
- return true;
149
- } catch (e: any) {
150
- error.value = e.message || 'Failed to update prompt';
151
- return false;
152
- } finally {
153
- loading.value = false;
154
- }
155
- }
156
-
157
- /**
158
- * Helper function to generate a single image via misc service
159
- */
160
- async function generateSingleImage(
161
- prompt: string,
162
- model: ImageModel = 'flux-kontext-pro',
163
- serviceUrl: string = DEFAULT_SERVICE_URL
164
- ): Promise<string | null> {
165
- const miscServiceUrl = `${serviceUrl}/api/misc/images/upload`;
166
-
167
- const response = await fetch(miscServiceUrl, {
168
- method: 'POST',
169
- headers: {
170
- 'Content-Type': 'application/json',
171
- },
172
- body: JSON.stringify({
173
- prompt,
174
- model,
175
- format: 'png',
176
- aspect_ratio: '16:9',
177
- })
178
- });
179
-
180
- if (!response.ok) {
181
- throw new Error(`Image generation failed: ${response.status}`);
182
- }
183
-
184
- const data = await response.json();
185
- return data?.id || null; // Backend returns 'id' not 'file_id'
186
- }
187
-
188
- /**
189
- * Regenerate example images for a style
190
- */
191
- async function regenerateStyleExamples(
192
- styleId: string,
193
- prompt: string,
194
- collection: string = 'ImageStyles',
195
- model: ImageModel = 'flux-kontext-pro',
196
- serviceUrl: string = DEFAULT_SERVICE_URL
197
- ): Promise<{ example_1: string; example_2: string } | null> {
198
- loading.value = true;
199
- error.value = null;
200
-
201
- try {
202
- // Generate two new images via misc service directly
203
- const [example_1, example_2] = await Promise.all([
204
- generateSingleImage(prompt, model, serviceUrl),
205
- generateSingleImage(prompt, model, serviceUrl),
206
- ]);
207
-
208
- if (example_1 && example_2) {
209
- // Update the style with new example images
210
- await api.patch(`/items/${collection}/${styleId}`, {
211
- example_1,
212
- example_2,
213
- });
214
- return { example_1, example_2 };
215
- }
216
-
217
- error.value = 'Failed to generate images';
218
- return null;
219
- } catch (e: any) {
220
- error.value = e.message || 'Failed to regenerate images';
221
- return null;
222
- } finally {
223
- loading.value = false;
224
- }
225
- }
226
-
227
- /**
228
- * Regenerate images for a specific placeholder using both prompts
229
- */
230
- async function regeneratePlaceholderImages(
231
- prompt1: string,
232
- prompt2: string,
233
- model: ImageModel = 'flux-kontext-pro',
234
- serviceUrl: string = DEFAULT_SERVICE_URL
235
- ): Promise<{ image1: string; image2: string } | null> {
236
- loading.value = true;
237
- error.value = null;
238
-
239
- try {
240
- // Generate one image per prompt
241
- const [image1, image2] = await Promise.all([
242
- generateSingleImage(prompt1, model, serviceUrl),
243
- generateSingleImage(prompt2, model, serviceUrl),
244
- ]);
245
-
246
- if (image1 && image2) {
247
- return { image1, image2 };
248
- }
249
-
250
- error.value = 'Failed to generate images';
251
- return null;
252
- } catch (e: any) {
253
- error.value = e.message || 'Failed to regenerate images';
254
- return null;
255
- } finally {
256
- loading.value = false;
257
- }
258
- }
259
-
260
- /**
261
- * Get the URL for a file by its ID
262
- */
263
- function getFileUrl(fileId: string | null): string {
264
- if (!fileId) return '';
265
- return `/assets/${fileId}?width=200&height=200&fit=cover`;
266
- }
267
-
268
- /**
269
- * Generate image prompts using the Python misc service
270
- * Calls the misc service directly (not through Directus)
271
- */
272
- async function generateImagePrompts(
273
- style: string,
274
- description: string,
275
- serviceUrl: string = DEFAULT_SERVICE_URL
276
- ): Promise<PromptGenerationResponse | null> {
277
- loading.value = true;
278
- error.value = null;
279
-
280
- try {
281
- const miscServiceUrl = `${serviceUrl}/api/misc/prompts`;
282
-
283
- const response = await fetch(miscServiceUrl, {
284
- method: 'POST',
285
- headers: {
286
- 'Content-Type': 'application/json',
287
- },
288
- body: JSON.stringify({
289
- style,
290
- description,
291
- model: 'gpt-4o-mini'
292
- })
293
- });
294
-
295
- if (!response.ok) {
296
- const errorData = await response.json().catch(() => ({ detail: response.statusText }));
297
- throw new Error(errorData.detail || `HTTP ${response.status}`);
298
- }
299
-
300
- return await response.json() as PromptGenerationResponse;
301
- } catch (e: any) {
302
- error.value = e.message || 'Failed to generate image prompts';
303
- return null;
304
- } finally {
305
- loading.value = false;
306
- }
307
- }
308
-
309
- /**
310
- * Get ImageVariants created for a specific lesson, including their images
311
- */
312
- async function getImageVariants(lessonId: string): Promise<ImageVariant[]> {
313
- loading.value = true;
314
- error.value = null;
315
-
316
- try {
317
- const response = await api.get('/items/ImageVariants', {
318
- params: {
319
- filter: { lesson: { _eq: lessonId } },
320
- fields: ['id', 'title', 'status', 'lesson', 'images.id', 'images.image', 'images.prompt', 'images.status'],
321
- },
322
- });
323
- return response.data.data || [];
324
- } catch (e: any) {
325
- error.value = e.message || 'Failed to fetch image variants';
326
- return [];
327
- } finally {
328
- loading.value = false;
329
- }
330
- }
331
-
332
- /**
333
- * Save generated images to ImageVariants and ContentImage collections
334
- */
335
- async function saveGeneratedImages(
336
- lessonId: string,
337
- items: Array<{
338
- placeholder: string;
339
- prompt1: string;
340
- prompt2: string;
341
- image1: string | null;
342
- image2: string | null;
343
- }>
344
- ): Promise<boolean> {
345
- loading.value = true;
346
- error.value = null;
347
-
348
- try {
349
- for (const item of items) {
350
- // Create ImageVariant record
351
- const variantResponse = await api.post('/items/ImageVariants', {
352
- title: item.placeholder,
353
- lesson: lessonId,
354
- status: 'found',
355
- });
356
-
357
- const variantId = variantResponse.data.data.id;
358
-
359
- // Create ContentImage records for each image
360
- const contentImages = [];
361
-
362
- if (item.image1) {
363
- contentImages.push({
364
- image: item.image1,
365
- prompt: item.prompt1,
366
- variants: variantId,
367
- status: 'generated',
368
- });
369
- }
370
-
371
- if (item.image2) {
372
- contentImages.push({
373
- image: item.image2,
374
- prompt: item.prompt2,
375
- variants: variantId,
376
- status: 'generated',
377
- });
378
- }
379
-
380
- // Batch create ContentImage records
381
- if (contentImages.length > 0) {
382
- await api.post('/items/ContentImage', contentImages);
383
- }
384
- }
385
-
386
- return true;
387
- } catch (e: any) {
388
- error.value = e.message || 'Failed to save generated images';
389
- return false;
390
- } finally {
391
- loading.value = false;
392
- }
393
- }
394
-
395
- /**
396
- * Delete an ImageVariant and its associated ContentImage records and files
397
- */
398
- async function deleteImageVariant(lessonId: string, placeholder: string): Promise<boolean> {
399
- loading.value = true;
400
- error.value = null;
401
-
402
- try {
403
- // Find the ImageVariant by lesson and title (placeholder), including image file IDs
404
- const response = await api.get('/items/ImageVariants', {
405
- params: {
406
- filter: {
407
- lesson: { _eq: lessonId },
408
- title: { _eq: placeholder }
409
- },
410
- fields: ['id', 'images.id', 'images.image'],
411
- },
412
- });
413
-
414
- const variants = response.data.data || [];
415
-
416
- for (const variant of variants) {
417
- // Collect file IDs to delete
418
- const fileIdsToDelete: string[] = [];
419
-
420
- // Delete associated ContentImage records first
421
- if (variant.images && variant.images.length > 0) {
422
- for (const img of variant.images) {
423
- // Collect the file ID if present
424
- if (img.image) {
425
- fileIdsToDelete.push(img.image);
426
- }
427
- // Delete the ContentImage record
428
- await api.delete(`/items/ContentImage/${img.id}`);
429
- }
430
- }
431
-
432
- // Delete the ImageVariant
433
- await api.delete(`/items/ImageVariants/${variant.id}`);
434
-
435
- // Delete the actual files from directus_files
436
- for (const fileId of fileIdsToDelete) {
437
- try {
438
- await api.delete(`/files/${fileId}`);
439
- } catch (fileError: any) {
440
- // Log but don't fail if file deletion fails (file might already be deleted)
441
- console.warn(`Failed to delete file ${fileId}:`, fileError.message);
442
- }
443
- }
444
- }
445
-
446
- return true;
447
- } catch (e: any) {
448
- error.value = e.message || 'Failed to delete image variant';
449
- return false;
450
- } finally {
451
- loading.value = false;
452
- }
453
- }
454
-
455
- /**
456
- * Extract [placeholder] patterns from content
457
- */
458
- function extractPlaceholders(content: string): string[] {
459
- const regex = /\[([^\]]+)\]/g;
460
- const matches: string[] = [];
461
- let match;
462
- while ((match = regex.exec(content)) !== null) {
463
- matches.push(match[1]);
464
- }
465
- return matches;
466
- }
467
-
468
- /**
469
- * Update lesson content by replacing [placeholder] with HTML image tag
470
- * Uses HTML format for WYSIWYG compatibility
471
- */
472
- function updateContentWithImages(
473
- content: string,
474
- selections: { caption: string; fileId: string }[]
475
- ): string {
476
- let updated = content;
477
- for (const { caption, fileId } of selections) {
478
- const placeholder = `[${caption}]`;
479
- const html = `<img src="/assets/${fileId}" alt="${caption}" />`;
480
- updated = updated.replace(placeholder, html);
481
- }
482
- return updated;
483
- }
484
-
485
- /**
486
- * Save updated content back to the lesson
487
- * Note: field name is 'description' (lowercase) in Directus schema
488
- */
489
- async function updateLessonContent(
490
- lessonId: string,
491
- content: string
492
- ): Promise<boolean> {
493
- loading.value = true;
494
- error.value = null;
495
-
496
- try {
497
- await api.patch(`/items/SM_Lessons/${lessonId}`, {
498
- description: content,
499
- });
500
- return true;
501
- } catch (e: any) {
502
- error.value = e.message || 'Failed to update lesson content';
503
- return false;
504
- } finally {
505
- loading.value = false;
506
- }
507
- }
508
-
509
- /**
510
- * Fetch lesson content from the API
511
- * This is more reliable than relying on inject('values') context
512
- */
513
- async function fetchLessonContent(lessonId: string): Promise<string | null> {
514
- try {
515
- const response = await api.get(`/items/SM_Lessons/${lessonId}`, {
516
- params: {
517
- fields: ['description'],
518
- },
519
- });
520
- // Try both lowercase and capitalized field names for compatibility
521
- return response.data.data?.description || response.data.data?.Description || null;
522
- } catch (e: any) {
523
- console.error('Failed to fetch lesson content:', e);
524
- return null;
525
- }
526
- }
527
-
528
- return {
529
- loading,
530
- error,
531
- fetchServiceUrl,
532
- updateServiceUrl,
533
- checkIsAdmin,
534
- fetchStyles,
535
- updateStylePrompt,
536
- regenerateStyleExamples,
537
- regeneratePlaceholderImages,
538
- getFileUrl,
539
- generateImagePrompts,
540
- getImageVariants,
541
- saveGeneratedImages,
542
- deleteImageVariant,
543
- extractPlaceholders,
544
- generateSingleImage,
545
- updateContentWithImages,
546
- updateLessonContent,
547
- fetchLessonContent,
548
- };
549
- }
package/src/index.ts DELETED
@@ -1,44 +0,0 @@
1
- import { defineInterface } from '@directus/extensions-sdk';
2
- import InterfaceComponent from './interface.vue';
3
-
4
- export default defineInterface({
5
- id: 'image-style-widget',
6
- name: 'Image Style Widget',
7
- icon: 'palette',
8
- description: 'Visual style selector and image reviewer',
9
- component: InterfaceComponent,
10
- options: [
11
- {
12
- field: 'mode',
13
- name: 'Mode',
14
- type: 'string',
15
- meta: {
16
- width: 'half',
17
- interface: 'select-dropdown',
18
- options: {
19
- choices: [
20
- { text: 'Style Selection', value: 'style-select' },
21
- { text: 'Image Review', value: 'image-review' },
22
- ],
23
- },
24
- },
25
- schema: {
26
- default_value: 'style-select',
27
- },
28
- },
29
- {
30
- field: 'stylesCollection',
31
- name: 'Styles Collection',
32
- type: 'string',
33
- meta: {
34
- width: 'half',
35
- interface: 'input',
36
- note: 'Collection containing style definitions (default: ImageStyles)',
37
- },
38
- schema: {
39
- default_value: 'ImageStyles',
40
- },
41
- },
42
- ],
43
- types: ['json', 'string'],
44
- });