Skip to content

Commit a2a074e

Browse files
committed
add feature image generation
1 parent 9a4625e commit a2a074e

9 files changed

Lines changed: 866 additions & 122 deletions

File tree

IMAGE_GENERATION_FEATURE.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Image Generation Feature for New Article Suggestions
2+
3+
## Overview
4+
Added ability to generate AI-powered featured images for "new_article" type suggestions.
5+
6+
## Changes Made
7+
8+
### 1. Database Migration
9+
**File:** `database/migrations/2025_10_10_095452_add_image_generation_to_ai_suggestions_table.php`
10+
- Added `generate_image` (boolean) - Toggle for image generation
11+
- Added `generated_image_url` (string) - URL of generated image
12+
- Added `image_generation_status` (string) - Status: pending, processing, completed, failed
13+
- Added `image_generation_error` (text) - Error details if generation fails
14+
15+
### 2. Model Updates
16+
**File:** `app/Models/AiSuggestion.php`
17+
- Added new fields to `$fillable` array
18+
- Added `generate_image` to `$casts` as boolean
19+
20+
### 3. New Job for Image Generation
21+
**File:** `app/Jobs/GenerateSuggestionImageJob.php`
22+
- Queued job that runs on `ai-processing` queue
23+
- Uses OpenAI DALL-E 3 for image generation
24+
- Generates 1792x1024 (landscape) images suitable for blog featured images
25+
- Automatically creates descriptive prompts from suggestion content
26+
- Handles errors and retries (max 3 attempts)
27+
- 120-second timeout per attempt
28+
29+
### 4. Controller Updates
30+
**File:** `app/Http/Controllers/Dashboard/PostAnalysisController.php`
31+
- Added `toggleImageGeneration()` method
32+
- Accepts boolean `generate_image` parameter
33+
- Dispatches `GenerateSuggestionImageJob` when enabled
34+
- Returns toast notifications for user feedback
35+
36+
### 5. Route Addition
37+
**File:** `routes/web.php`
38+
- Added POST route: `/suggestions/{suggestion}/toggle-image`
39+
- Route name: `suggestions.toggle-image`
40+
41+
### 6. Frontend Updates
42+
**File:** `resources/js/pages/Dashboard/PostAnalysis/Show.tsx`
43+
- Added image generation fields to `Suggestion` interface
44+
- Added checkbox UI for "Generate Featured Image" (only for `new_article` type)
45+
- Shows generation status with badges (pending, processing, completed, failed)
46+
- Displays link to view generated image when completed
47+
- Added `handleToggleImageGeneration()` handler function
48+
- Integrated with toast notifications for user feedback
49+
50+
## How It Works
51+
52+
1. **User Interface:**
53+
- When viewing a post analysis with `new_article` suggestions
54+
- A checkbox appears: "Generate Featured Image"
55+
- User can toggle it on/off
56+
57+
2. **Backend Process:**
58+
- When enabled, dispatches `GenerateSuggestionImageJob` to queue
59+
- Job generates image prompt from suggestion content
60+
- Calls OpenAI DALL-E 3 API
61+
- Saves image URL to database
62+
- Updates status throughout the process
63+
64+
3. **Status Flow:**
65+
- `pending` → User just enabled generation
66+
- `processing` → Job is actively generating image
67+
- `completed` → Image generated successfully (URL available)
68+
- `failed` → Generation failed (error message stored)
69+
- `skipped` → User disabled generation
70+
71+
## Configuration Required
72+
73+
### OpenAI API Key
74+
Ensure `OPENAI_API_KEY` is set in `.env`:
75+
```env
76+
OPENAI_API_KEY=sk-...
77+
```
78+
79+
### Queue Worker
80+
The queue worker must be running to process image generation:
81+
```bash
82+
php artisan queue:work database --queue=ai-processing,default
83+
```
84+
85+
## API Usage
86+
87+
### OpenAI DALL-E 3
88+
- **Model:** `dall-e-3`
89+
- **Size:** `1792x1024` (landscape, ideal for blog headers)
90+
- **Quality:** `standard`
91+
- **Images per request:** 1
92+
93+
## Files Changed
94+
95+
1. `database/migrations/2025_10_10_095452_add_image_generation_to_ai_suggestions_table.php` - NEW
96+
2. `app/Models/AiSuggestion.php` - MODIFIED
97+
3. `app/Jobs/GenerateSuggestionImageJob.php` - NEW
98+
4. `app/Http/Controllers/Dashboard/PostAnalysisController.php` - MODIFIED
99+
5. `routes/web.php` - MODIFIED
100+
6. `resources/js/pages/Dashboard/PostAnalysis/Show.tsx` - MODIFIED
101+
102+
## Testing Checklist
103+
104+
- [ ] Run migration: `php artisan migrate`
105+
- [ ] Build frontend: `npm run build`
106+
- [ ] Start queue worker
107+
- [ ] Create a new article suggestion
108+
- [ ] Toggle "Generate Featured Image"
109+
- [ ] Verify job is queued
110+
- [ ] Check image generation completes
111+
- [ ] Verify image URL is saved
112+
- [ ] Test "View Image" link
113+
- [ ] Test error handling (invalid API key, etc.)
114+
115+
## Future Enhancements
116+
117+
- Support for other AI image providers (Stability AI, Midjourney)
118+
- Custom image size/aspect ratio selection
119+
- Image style preferences (realistic, illustration, abstract, etc.)
120+
- Download and store images locally instead of external URLs
121+
- Bulk image generation for multiple suggestions
122+
- Image editing/regeneration options
123+
124+
## Notes
125+
126+
- Only visible for `type === 'new_article'` suggestions
127+
- Does not break existing suggestions (all new fields are optional)
128+
- Graceful error handling if OpenAI API fails
129+
- Images remain accessible via URL (not stored locally by default)
130+
- Queue worker required for background processing

app/Http/Controllers/Api/SuggestionController.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public function getSuggestions(Request $request, $analysisId): JsonResponse
7070
'meta_data' => $suggestion->meta_data,
7171
'created_at' => $suggestion->created_at,
7272
'implemented_at' => $suggestion->implemented_at,
73+
// Image generation fields
74+
'generate_image' => $suggestion->generate_image,
75+
'generated_image_url' => $suggestion->generated_image_url,
76+
'image_generation_status' => $suggestion->image_generation_status,
77+
'has_generated_image' => !empty($suggestion->generated_image_url) && $suggestion->image_generation_status === 'completed',
7378
];
7479
}),
7580
'summary' => [
@@ -340,6 +345,12 @@ public function getSuggestionDetail(Request $request, $suggestionId): JsonRespon
340345
'created_at' => $suggestion->created_at,
341346
'updated_at' => $suggestion->updated_at,
342347
'implemented_at' => $suggestion->implemented_at,
348+
// Image generation fields
349+
'generate_image' => $suggestion->generate_image,
350+
'generated_image_url' => $suggestion->generated_image_url,
351+
'image_generation_status' => $suggestion->image_generation_status,
352+
'image_generation_error' => $suggestion->image_generation_error,
353+
'has_generated_image' => !empty($suggestion->generated_image_url) && $suggestion->image_generation_status === 'completed',
343354
],
344355
'analysis' => [
345356
'id' => $suggestion->articleAnalysis->id,
@@ -496,4 +507,140 @@ private function formatAsMarkdown(array $data): string
496507

497508
return $markdown;
498509
}
510+
511+
/**
512+
* Toggle image generation for a suggestion (WordPress API)
513+
*/
514+
public function toggleImageGeneration(Request $request, $suggestionId): JsonResponse
515+
{
516+
$validated = $request->validate([
517+
'generate_image' => 'required|boolean',
518+
]);
519+
520+
try {
521+
$suggestion = AiSuggestion::with(['articleAnalysis.wordpressPost'])
522+
->where('id', $suggestionId)
523+
->first();
524+
525+
if (!$suggestion) {
526+
return response()->json([
527+
'success' => false,
528+
'message' => 'Suggestion not found',
529+
], 404);
530+
}
531+
532+
// Check authorization
533+
$wordPressSite = $request->attributes->get('wordpress_site');
534+
if ($suggestion->articleAnalysis->wordpressPost->wordpress_site_id !== $wordPressSite->id) {
535+
return response()->json([
536+
'success' => false,
537+
'message' => 'Unauthorized access',
538+
], 403);
539+
}
540+
541+
// Only allow for new_article type suggestions
542+
if ($suggestion->type !== 'new_article') {
543+
return response()->json([
544+
'success' => false,
545+
'message' => 'Image generation is only available for new article suggestions',
546+
], 400);
547+
}
548+
549+
// Update the suggestion
550+
$suggestion->update([
551+
'generate_image' => $validated['generate_image'],
552+
'image_generation_status' => $validated['generate_image'] ? 'pending' : 'skipped',
553+
]);
554+
555+
// If enabled, dispatch the image generation job
556+
if ($validated['generate_image']) {
557+
\App\Jobs\GenerateSuggestionImageJob::dispatch($suggestion->id);
558+
559+
return response()->json([
560+
'success' => true,
561+
'message' => 'Image generation queued successfully',
562+
'data' => [
563+
'suggestion_id' => $suggestion->id,
564+
'generate_image' => true,
565+
'image_generation_status' => 'pending',
566+
'estimated_time' => '3-5 seconds',
567+
],
568+
]);
569+
}
570+
571+
return response()->json([
572+
'success' => true,
573+
'message' => 'Image generation disabled',
574+
'data' => [
575+
'suggestion_id' => $suggestion->id,
576+
'generate_image' => false,
577+
'image_generation_status' => 'skipped',
578+
],
579+
]);
580+
581+
} catch (\Exception $e) {
582+
Log::error('Toggle image generation failed', [
583+
'suggestion_id' => $suggestionId,
584+
'error' => $e->getMessage(),
585+
]);
586+
587+
return response()->json([
588+
'success' => false,
589+
'message' => 'Failed to toggle image generation',
590+
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error',
591+
], 500);
592+
}
593+
}
594+
595+
/**
596+
* Get image generation status for a suggestion (WordPress API)
597+
*/
598+
public function getImageStatus(Request $request, $suggestionId): JsonResponse
599+
{
600+
try {
601+
$suggestion = AiSuggestion::with(['articleAnalysis.wordpressPost'])
602+
->where('id', $suggestionId)
603+
->first();
604+
605+
if (!$suggestion) {
606+
return response()->json([
607+
'success' => false,
608+
'message' => 'Suggestion not found',
609+
], 404);
610+
}
611+
612+
// Check authorization
613+
$wordPressSite = $request->attributes->get('wordpress_site');
614+
if ($suggestion->articleAnalysis->wordpressPost->wordpress_site_id !== $wordPressSite->id) {
615+
return response()->json([
616+
'success' => false,
617+
'message' => 'Unauthorized access',
618+
], 403);
619+
}
620+
621+
return response()->json([
622+
'success' => true,
623+
'data' => [
624+
'suggestion_id' => $suggestion->id,
625+
'generate_image' => $suggestion->generate_image,
626+
'image_generation_status' => $suggestion->image_generation_status,
627+
'generated_image_url' => $suggestion->generated_image_url,
628+
'image_generation_error' => $suggestion->image_generation_error,
629+
'has_generated_image' => !empty($suggestion->generated_image_url) && $suggestion->image_generation_status === 'completed',
630+
],
631+
]);
632+
633+
} catch (\Exception $e) {
634+
Log::error('Get image status failed', [
635+
'suggestion_id' => $suggestionId,
636+
'error' => $e->getMessage(),
637+
]);
638+
639+
return response()->json([
640+
'success' => false,
641+
'message' => 'Failed to get image status',
642+
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error',
643+
], 500);
644+
}
645+
}
499646
}

app/Http/Controllers/Dashboard/PostAnalysisController.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ public function show(WordPressPost $post): Response
241241
'status' => $suggestion->status,
242242
'implemented_at' => $suggestion->implemented_at?->format('M d, Y H:i'),
243243
'created_at' => $suggestion->created_at->format('M d, Y H:i'),
244+
// Image generation fields
245+
'generate_image' => $suggestion->generate_image,
246+
'generated_image_url' => $suggestion->generated_image_url,
247+
'image_generation_status' => $suggestion->image_generation_status,
248+
'image_generation_error' => $suggestion->image_generation_error,
244249
];
245250
});
246251

@@ -410,4 +415,60 @@ public function compareVersions(Request $request, WordPressPost $post)
410415
'diff' => $diff,
411416
]);
412417
}
418+
419+
/**
420+
* Toggle image generation for a suggestion
421+
*/
422+
public function toggleImageGeneration(Request $request, AiSuggestion $suggestion)
423+
{
424+
$this->authorize('update', $suggestion->wordPressPost);
425+
426+
$validated = $request->validate([
427+
'generate_image' => 'required|boolean',
428+
]);
429+
430+
try {
431+
$suggestion->update([
432+
'generate_image' => $validated['generate_image'],
433+
'image_generation_status' => $validated['generate_image'] ? 'pending' : 'skipped',
434+
]);
435+
436+
// If enabled, dispatch the image generation job
437+
if ($validated['generate_image']) {
438+
\App\Jobs\GenerateSuggestionImageJob::dispatch($suggestion->id);
439+
440+
return back()->with([
441+
'success' => 'Image generation queued successfully',
442+
'toast' => [
443+
'type' => 'success',
444+
'message' => 'Image generation started',
445+
'description' => 'The AI is generating an image for this suggestion.',
446+
]
447+
]);
448+
}
449+
450+
return back()->with([
451+
'success' => 'Image generation disabled',
452+
'toast' => [
453+
'type' => 'success',
454+
'message' => 'Image generation disabled',
455+
]
456+
]);
457+
458+
} catch (\Exception $e) {
459+
Log::error('Failed to toggle image generation', [
460+
'suggestion_id' => $suggestion->id,
461+
'error' => $e->getMessage(),
462+
]);
463+
464+
return back()->with([
465+
'error' => 'Failed to toggle image generation',
466+
'toast' => [
467+
'type' => 'error',
468+
'message' => 'Error',
469+
'description' => $e->getMessage(),
470+
]
471+
]);
472+
}
473+
}
413474
}

0 commit comments

Comments
 (0)