Add Image Generation Settings Endpoint and Update Frontend Modal: Implement a new API endpoint to fetch image generation settings, enhance the ImageQueueModal to display progress and status, and integrate the settings into the image generation workflow.
This commit is contained in:
@@ -731,6 +731,72 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
'error': f'Failed to get settings: {str(e)}'
|
'error': f'Failed to get settings: {str(e)}'
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
|
||||||
|
def get_image_generation_settings(self, request):
|
||||||
|
"""Get image generation settings for current account"""
|
||||||
|
account = getattr(request, 'account', None)
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
# Fallback to user's account
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||||
|
account = getattr(user, 'account', None)
|
||||||
|
# Fallback to default account
|
||||||
|
if not account:
|
||||||
|
from igny8_core.auth.models import Account
|
||||||
|
try:
|
||||||
|
account = Account.objects.first()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
return Response({
|
||||||
|
'error': 'Account not found',
|
||||||
|
'type': 'AuthenticationError'
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .models import IntegrationSettings
|
||||||
|
integration = IntegrationSettings.objects.get(
|
||||||
|
account=account,
|
||||||
|
integration_type='image_generation',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
config = integration.config or {}
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'success': True,
|
||||||
|
'config': {
|
||||||
|
'provider': config.get('provider', 'openai'),
|
||||||
|
'model': config.get('model', 'dall-e-3'),
|
||||||
|
'image_type': config.get('image_type', 'realistic'),
|
||||||
|
'max_in_article_images': config.get('max_in_article_images', 2),
|
||||||
|
'image_format': config.get('image_format', 'webp'),
|
||||||
|
'desktop_enabled': config.get('desktop_enabled', True),
|
||||||
|
'mobile_enabled': config.get('mobile_enabled', True),
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
except IntegrationSettings.DoesNotExist:
|
||||||
|
return Response({
|
||||||
|
'success': True,
|
||||||
|
'config': {
|
||||||
|
'provider': 'openai',
|
||||||
|
'model': 'dall-e-3',
|
||||||
|
'image_type': 'realistic',
|
||||||
|
'max_in_article_images': 2,
|
||||||
|
'image_format': 'webp',
|
||||||
|
'desktop_enabled': True,
|
||||||
|
'mobile_enabled': True,
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
|
||||||
|
return Response({
|
||||||
|
'error': str(e),
|
||||||
|
'type': 'ServerError'
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'], url_path='task_progress/(?P<task_id>[^/.]+)', url_name='task-progress')
|
@action(detail=False, methods=['get'], url_path='task_progress/(?P<task_id>[^/.]+)', url_name='task-progress')
|
||||||
def task_progress(self, request, task_id=None):
|
def task_progress(self, request, task_id=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ integration_task_progress_viewset = IntegrationSettingsViewSet.as_view({
|
|||||||
'get': 'task_progress',
|
'get': 'task_progress',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
|
||||||
|
'get': 'get_image_generation_settings',
|
||||||
|
})
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
# System status endpoint
|
# System status endpoint
|
||||||
@@ -55,6 +59,8 @@ urlpatterns = [
|
|||||||
path('webhook/', gitea_webhook, name='gitea-webhook'),
|
path('webhook/', gitea_webhook, name='gitea-webhook'),
|
||||||
# Integration settings routes - exact match to reference plugin workflow
|
# Integration settings routes - exact match to reference plugin workflow
|
||||||
# IMPORTANT: More specific paths must come BEFORE less specific ones
|
# IMPORTANT: More specific paths must come BEFORE less specific ones
|
||||||
|
# GET: Image generation settings - MUST come before other settings paths
|
||||||
|
path('integrations/image_generation/', integration_image_gen_settings_viewset, name='integration-image-gen-settings'),
|
||||||
# GET: Task progress - MUST come before other settings paths
|
# GET: Task progress - MUST come before other settings paths
|
||||||
path('settings/task_progress/<str:task_id>/', integration_task_progress_viewset, name='integration-task-progress'),
|
path('settings/task_progress/<str:task_id>/', integration_task_progress_viewset, name='integration-task-progress'),
|
||||||
# POST: Generate image (for image_generation integration) - MUST come before base path
|
# POST: Generate image (for image_generation integration) - MUST come before base path
|
||||||
|
|||||||
240
docs/IMAGE_GENERATION_CHANGELOG.md
Normal file
240
docs/IMAGE_GENERATION_CHANGELOG.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Image Generation Implementation Changelog
|
||||||
|
|
||||||
|
## Stage 1: Pre-Queue Modal Display - Completed ✅
|
||||||
|
|
||||||
|
**Date:** 2025-01-XX
|
||||||
|
**Status:** Completed
|
||||||
|
**Goal:** Open modal immediately showing all progress bars before any API calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Frontend Components
|
||||||
|
|
||||||
|
#### Created: `frontend/src/components/common/ImageQueueModal.tsx`
|
||||||
|
- **Purpose:** Display image generation queue with individual progress bars
|
||||||
|
- **Features:**
|
||||||
|
- Shows all images that will be generated with individual progress bars
|
||||||
|
- Displays queue number, label, content title, status, and progress percentage
|
||||||
|
- Includes thumbnail placeholder for generated images
|
||||||
|
- Supports 4 states: `pending`, `processing`, `completed`, `failed`
|
||||||
|
- Styled similar to WP plugin's image-queue-processor.js modal
|
||||||
|
- Responsive design with dark mode support
|
||||||
|
- Prevents closing while processing
|
||||||
|
- Shows completion summary in footer
|
||||||
|
|
||||||
|
**Key Props:**
|
||||||
|
- `isOpen`: Boolean to control modal visibility
|
||||||
|
- `onClose`: Callback when modal is closed
|
||||||
|
- `queue`: Array of `ImageQueueItem` objects
|
||||||
|
- `totalImages`: Total number of images in queue
|
||||||
|
- `onUpdateQueue`: Optional callback to update queue state
|
||||||
|
|
||||||
|
**ImageQueueItem Interface:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
imageId: number | null;
|
||||||
|
index: number;
|
||||||
|
label: string;
|
||||||
|
type: 'featured' | 'in_article';
|
||||||
|
position?: number;
|
||||||
|
contentTitle: string;
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||||
|
progress: number; // 0-100
|
||||||
|
imageUrl: string | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Updated: `frontend/src/pages/Writer/Images.tsx`
|
||||||
|
- **Added Imports:**
|
||||||
|
- `fetchImageGenerationSettings` from `../../services/api`
|
||||||
|
- `ImageQueueModal` and `ImageQueueItem` from `../../components/common/ImageQueueModal`
|
||||||
|
|
||||||
|
- **Added State:**
|
||||||
|
```typescript
|
||||||
|
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
|
||||||
|
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
|
||||||
|
const [currentContentId, setCurrentContentId] = useState<number | null>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Added Function: `buildImageQueue()`**
|
||||||
|
- Builds image queue structure from content images
|
||||||
|
- Includes featured image (if pending and has prompt)
|
||||||
|
- Includes in-article images (up to `max_in_article_images` from settings)
|
||||||
|
- Sorts in-article images by position
|
||||||
|
- Returns array of `ImageQueueItem` objects
|
||||||
|
|
||||||
|
- **Updated Function: `handleGenerateImages()`**
|
||||||
|
- **Stage 1 Implementation:**
|
||||||
|
1. Fetches image generation settings to get `max_in_article_images`
|
||||||
|
2. Builds image queue using `buildImageQueue()`
|
||||||
|
3. Opens modal immediately with all progress bars at 0%
|
||||||
|
4. Collects image IDs for future API call (Stage 2)
|
||||||
|
5. Logs Stage 1 completion
|
||||||
|
|
||||||
|
- **Added Modal Component:**
|
||||||
|
- Renders `ImageQueueModal` at end of component
|
||||||
|
- Handles modal close with image reload
|
||||||
|
- Passes queue state and update callback
|
||||||
|
|
||||||
|
### 2. API Services
|
||||||
|
|
||||||
|
#### Updated: `frontend/src/services/api.ts`
|
||||||
|
- **Added Interface: `ImageGenerationSettings`**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: boolean;
|
||||||
|
config: {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
image_type: string;
|
||||||
|
max_in_article_images: number;
|
||||||
|
image_format: string;
|
||||||
|
desktop_enabled: boolean;
|
||||||
|
mobile_enabled: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Added Function: `fetchImageGenerationSettings()`**
|
||||||
|
- Fetches image generation settings from backend
|
||||||
|
- Endpoint: `/v1/system/integrations/image_generation/`
|
||||||
|
- Returns settings including `max_in_article_images`
|
||||||
|
|
||||||
|
### 3. Backend API
|
||||||
|
|
||||||
|
#### Updated: `backend/igny8_core/modules/system/integration_views.py`
|
||||||
|
- **Added Method: `get_image_generation_settings()`**
|
||||||
|
- Action decorator: `@action(detail=False, methods=['get'], url_path='image_generation')`
|
||||||
|
- Gets account from request (with fallbacks)
|
||||||
|
- Retrieves `IntegrationSettings` for `image_generation` type
|
||||||
|
- Returns formatted config with defaults if not found
|
||||||
|
- Default values:
|
||||||
|
- `provider`: 'openai'
|
||||||
|
- `model`: 'dall-e-3'
|
||||||
|
- `image_type`: 'realistic'
|
||||||
|
- `max_in_article_images`: 2
|
||||||
|
- `image_format`: 'webp'
|
||||||
|
- `desktop_enabled`: True
|
||||||
|
- `mobile_enabled`: True
|
||||||
|
|
||||||
|
#### Updated: `backend/igny8_core/modules/system/urls.py`
|
||||||
|
- **Added URL Route:**
|
||||||
|
```python
|
||||||
|
path('integrations/image_generation/', integration_image_gen_settings_viewset, name='integration-image-gen-settings')
|
||||||
|
```
|
||||||
|
- **Added ViewSet:**
|
||||||
|
```python
|
||||||
|
integration_image_gen_settings_viewset = IntegrationSettingsViewSet.as_view({
|
||||||
|
'get': 'get_image_generation_settings',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### User Flow:
|
||||||
|
1. User clicks "Generate Images" button on Images page
|
||||||
|
2. System fetches `max_in_article_images` from IntegrationSettings
|
||||||
|
3. System builds queue:
|
||||||
|
- 1 Featured Image (if pending and has prompt)
|
||||||
|
- N In-Article Images (up to `max_in_article_images`, if pending and have prompts)
|
||||||
|
4. **Modal opens immediately** showing all progress bars at 0%
|
||||||
|
5. Each progress bar displays:
|
||||||
|
- Queue number (1, 2, 3...)
|
||||||
|
- Label (Featured Image, In-Article Image 1, etc.)
|
||||||
|
- Content title
|
||||||
|
- Status: "⏳ Pending"
|
||||||
|
- Progress: 0%
|
||||||
|
- Thumbnail placeholder: "No image"
|
||||||
|
|
||||||
|
### Queue Calculation:
|
||||||
|
```
|
||||||
|
Total Images = 1 (featured) + min(pending_in_article_count, max_in_article_images)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- Settings: `max_in_article_images = 3`
|
||||||
|
- Content has: 1 featured (pending), 5 in-article (pending)
|
||||||
|
- Queue: 1 featured + 3 in-article = **4 total progress bars**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### Frontend:
|
||||||
|
1. ✅ `frontend/src/components/common/ImageQueueModal.tsx` (NEW)
|
||||||
|
2. ✅ `frontend/src/pages/Writer/Images.tsx` (UPDATED)
|
||||||
|
3. ✅ `frontend/src/services/api.ts` (UPDATED)
|
||||||
|
|
||||||
|
### Backend:
|
||||||
|
1. ✅ `backend/igny8_core/modules/system/integration_views.py` (UPDATED)
|
||||||
|
2. ✅ `backend/igny8_core/modules/system/urls.py` (UPDATED)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [x] Modal opens immediately when "Generate Images" button is clicked
|
||||||
|
- [x] Modal shows correct number of progress bars (1 featured + N in-article)
|
||||||
|
- [x] Progress bars display correct labels and content titles
|
||||||
|
- [x] All progress bars start at 0% with "Pending" status
|
||||||
|
- [x] Modal can be closed (when not processing)
|
||||||
|
- [x] API endpoint returns correct `max_in_article_images` value
|
||||||
|
- [x] Queue respects `max_in_article_images` setting
|
||||||
|
- [x] Only pending images with prompts are included in queue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Stage 2)
|
||||||
|
|
||||||
|
### Planned Features:
|
||||||
|
1. **Start Actual Generation**
|
||||||
|
- Call `generateImages()` API with image IDs
|
||||||
|
- Handle async task response with `task_id`
|
||||||
|
|
||||||
|
2. **Real-Time Progress Updates**
|
||||||
|
- Poll task progress or use WebSocket
|
||||||
|
- Update individual progress bars as images are generated
|
||||||
|
- Implement progressive loading (0-50% in 7s, 50-75% in 5s, etc.)
|
||||||
|
|
||||||
|
3. **Sequential Processing**
|
||||||
|
- Process images one at a time (sequential)
|
||||||
|
- Update status: pending → processing → completed/failed
|
||||||
|
- Update progress percentage for each image
|
||||||
|
|
||||||
|
4. **Error Handling**
|
||||||
|
- Mark failed images with error message
|
||||||
|
- Continue processing other images if one fails
|
||||||
|
- Display error in queue item
|
||||||
|
|
||||||
|
5. **Completion Handling**
|
||||||
|
- Show generated image thumbnails
|
||||||
|
- Update all statuses to completed/failed
|
||||||
|
- Reload images list on modal close
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Stage 1 focuses on **immediate visual feedback** - modal opens instantly
|
||||||
|
- No API calls are made in Stage 1 (only settings fetch)
|
||||||
|
- Queue structure is built client-side from existing image data
|
||||||
|
- Modal is ready to receive progress updates in Stage 2
|
||||||
|
- Design matches WP plugin's image-queue-processor.js modal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- **Reference Implementation:** `igny8-ai-seo-wp-plugin/assets/js/image-queue-processor.js`
|
||||||
|
- **Implementation Plan:** `docs/IMAGE_GENERATION_IMPLEMENTATION_PLAN.md`
|
||||||
|
- **Backend Function:** `backend/igny8_core/ai/functions/generate_images_from_prompts.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Stage 1 Changelog**
|
||||||
|
|
||||||
@@ -1,131 +1,81 @@
|
|||||||
/**
|
/**
|
||||||
* Image Queue Modal - Shows per-image progress for image generation
|
* ImageQueueModal - Displays image generation queue with individual progress bars
|
||||||
* Similar to WordPress plugin's image queue processor
|
* Similar to WP plugin's image-queue-processor.js modal
|
||||||
|
* Stage 1: Shows all progress bars immediately when Generate button is clicked
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import { Modal } from '../ui/modal';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { CloseIcon as XIcon } from '../../icons';
|
||||||
|
|
||||||
export interface ImageQueueItem {
|
export interface ImageQueueItem {
|
||||||
image_id: number;
|
imageId: number | null;
|
||||||
index: number;
|
index: number;
|
||||||
label: string;
|
label: string;
|
||||||
content_title: string;
|
type: 'featured' | 'in_article';
|
||||||
|
position?: number;
|
||||||
|
contentTitle: string;
|
||||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||||
progress: number;
|
progress: number;
|
||||||
image_url: string | null;
|
imageUrl: string | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImageQueueModalProps {
|
interface ImageQueueModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
queue: ImageQueueItem[];
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
queue: ImageQueueItem[];
|
||||||
|
totalImages: number;
|
||||||
|
onUpdateQueue?: (queue: ImageQueueItem[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImageQueueModal({
|
export default function ImageQueueModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
queue,
|
|
||||||
onClose,
|
onClose,
|
||||||
|
queue,
|
||||||
|
totalImages,
|
||||||
|
onUpdateQueue,
|
||||||
}: ImageQueueModalProps) {
|
}: ImageQueueModalProps) {
|
||||||
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
|
const [localQueue, setLocalQueue] = useState<ImageQueueItem[]>(queue);
|
||||||
const progressIntervalsRef = useRef<Map<number, NodeJS.Timeout>>(new Map());
|
|
||||||
|
|
||||||
// Update local queue when prop changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalQueue(queue);
|
setLocalQueue(queue);
|
||||||
}, [queue]);
|
}, [queue]);
|
||||||
|
|
||||||
// Simulate smooth progress for processing images (like WP plugin)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (onUpdateQueue) {
|
||||||
|
onUpdateQueue(localQueue);
|
||||||
|
}
|
||||||
|
}, [localQueue, onUpdateQueue]);
|
||||||
|
|
||||||
localQueue.forEach((item) => {
|
if (!isOpen) return null;
|
||||||
if (item.status === 'processing' && item.progress < 95) {
|
|
||||||
// Clear existing interval for this item
|
|
||||||
const existing = progressIntervalsRef.current.get(item.image_id);
|
|
||||||
if (existing) {
|
|
||||||
clearInterval(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progressive loading: 50% in 7s, 75% in next 5s, then 5% every second until 95%
|
const getStatusIcon = (status: string) => {
|
||||||
let currentProgress = item.progress;
|
|
||||||
let phase = 1;
|
|
||||||
let phaseStartTime = Date.now();
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const elapsed = Date.now() - phaseStartTime;
|
|
||||||
|
|
||||||
if (phase === 1 && currentProgress < 50) {
|
|
||||||
// Phase 1: 0% to 50% in 7 seconds (7.14% per second)
|
|
||||||
currentProgress += 0.714;
|
|
||||||
if (currentProgress >= 50 || elapsed >= 7000) {
|
|
||||||
currentProgress = 50;
|
|
||||||
phase = 2;
|
|
||||||
phaseStartTime = Date.now();
|
|
||||||
}
|
|
||||||
} else if (phase === 2 && currentProgress < 75) {
|
|
||||||
// Phase 2: 50% to 75% in 5 seconds (5% per second)
|
|
||||||
currentProgress += 0.5;
|
|
||||||
if (currentProgress >= 75 || elapsed >= 5000) {
|
|
||||||
currentProgress = 75;
|
|
||||||
phase = 3;
|
|
||||||
phaseStartTime = Date.now();
|
|
||||||
}
|
|
||||||
} else if (phase === 3 && currentProgress < 95) {
|
|
||||||
// Phase 3: 75% to 95% - 5% every second
|
|
||||||
if (elapsed >= 1000) {
|
|
||||||
currentProgress = Math.min(95, currentProgress + 5);
|
|
||||||
phaseStartTime = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local state
|
|
||||||
setLocalQueue((prev) =>
|
|
||||||
prev.map((q) =>
|
|
||||||
q.image_id === item.image_id
|
|
||||||
? { ...q, progress: Math.min(95, currentProgress) }
|
|
||||||
: q
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
progressIntervalsRef.current.set(item.image_id, interval);
|
|
||||||
} else {
|
|
||||||
// Clear interval if not processing
|
|
||||||
const existing = progressIntervalsRef.current.get(item.image_id);
|
|
||||||
if (existing) {
|
|
||||||
clearInterval(existing);
|
|
||||||
progressIntervalsRef.current.delete(item.image_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Cleanup intervals on unmount
|
|
||||||
progressIntervalsRef.current.forEach((interval) => clearInterval(interval));
|
|
||||||
progressIntervalsRef.current.clear();
|
|
||||||
};
|
|
||||||
}, [isOpen, localQueue]);
|
|
||||||
|
|
||||||
// Check if all images are completed
|
|
||||||
const allCompleted = localQueue.every(
|
|
||||||
(item) => item.status === 'completed' || item.status === 'failed'
|
|
||||||
);
|
|
||||||
const completedCount = localQueue.filter(
|
|
||||||
(item) => item.status === 'completed'
|
|
||||||
).length;
|
|
||||||
const failedCount = localQueue.filter((item) => item.status === 'failed').length;
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'pending':
|
||||||
return 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800';
|
return '⏳';
|
||||||
case 'failed':
|
|
||||||
return 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800';
|
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800';
|
return '🔄';
|
||||||
|
case 'completed':
|
||||||
|
return '✅';
|
||||||
|
case 'failed':
|
||||||
|
return '❌';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-50 border-gray-200 dark:bg-gray-800 dark:border-gray-700';
|
return '⏳';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'Pending';
|
||||||
|
case 'processing':
|
||||||
|
return 'Generating...';
|
||||||
|
case 'completed':
|
||||||
|
return 'Complete';
|
||||||
|
case 'failed':
|
||||||
|
return 'Failed';
|
||||||
|
default:
|
||||||
|
return 'Pending';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,140 +92,127 @@ export default function ImageQueueModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
const isProcessing = localQueue.some(item => item.status === 'processing');
|
||||||
switch (status) {
|
const completedCount = localQueue.filter(item => item.status === 'completed').length;
|
||||||
case 'completed':
|
const failedCount = localQueue.filter(item => item.status === 'failed').length;
|
||||||
return '✅';
|
const allDone = localQueue.every(item => item.status === 'completed' || item.status === 'failed');
|
||||||
case 'failed':
|
|
||||||
return '❌';
|
|
||||||
case 'processing':
|
|
||||||
return '⏳';
|
|
||||||
default:
|
|
||||||
return '⏸️';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return 'Complete';
|
|
||||||
case 'failed':
|
|
||||||
return 'Failed';
|
|
||||||
case 'processing':
|
|
||||||
return 'Generating...';
|
|
||||||
default:
|
|
||||||
return 'Pending';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||||
isOpen={isOpen}
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
onClose={onClose}
|
|
||||||
className="max-w-4xl"
|
|
||||||
showCloseButton={!allCompleted}
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6">
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1 text-center">
|
<div>
|
||||||
🎨 Generating Images
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
</h3>
|
🎨 Generating Images
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
</h3>
|
||||||
Total: {localQueue.length} image{localQueue.length !== 1 ? 's' : ''} in queue
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
{allCompleted && (
|
Total: {totalImages} image{totalImages !== 1 ? 's' : ''} in queue
|
||||||
<span className="ml-2">
|
|
||||||
({completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Queue */}
|
|
||||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
|
|
||||||
{localQueue.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.image_id}
|
|
||||||
className={`p-4 rounded-lg border-2 transition-colors ${getStatusColor(
|
|
||||||
item.status
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* Left side: Info and progress */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold flex items-center justify-center flex-shrink-0">
|
|
||||||
{item.index}
|
|
||||||
</div>
|
|
||||||
<span className="font-semibold text-sm text-gray-900 dark:text-white">
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400 flex-1 truncate">
|
|
||||||
{item.content_title}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
|
||||||
{getStatusIcon(item.status)} {getStatusText(item.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar */}
|
|
||||||
<div className="relative h-5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={`h-full ${getProgressColor(
|
|
||||||
item.status
|
|
||||||
)} transition-all duration-300 ease-out`}
|
|
||||||
style={{ width: `${item.progress}%` }}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="text-xs font-bold text-gray-900 dark:text-white">
|
|
||||||
{item.status === 'processing'
|
|
||||||
? `${Math.round(item.progress)}%`
|
|
||||||
: item.status === 'completed'
|
|
||||||
? '100%'
|
|
||||||
: item.status === 'failed'
|
|
||||||
? 'Failed'
|
|
||||||
: '0%'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error message */}
|
|
||||||
{item.status === 'failed' && item.error && (
|
|
||||||
<div className="mt-2 p-2 bg-red-100 dark:bg-red-900/30 border-l-3 border-red-500 rounded text-xs text-red-700 dark:text-red-300">
|
|
||||||
{item.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side: Thumbnail */}
|
|
||||||
<div className="w-20 h-20 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden flex-shrink-0">
|
|
||||||
{item.image_url ? (
|
|
||||||
<img
|
|
||||||
src={item.image_url}
|
|
||||||
alt={item.label}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-gray-400">No image</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Success message when all done */}
|
|
||||||
{allCompleted && (
|
|
||||||
<div className="mt-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
||||||
<p className="text-sm text-green-800 dark:text-green-200 text-center font-medium">
|
|
||||||
Image generation complete! {completedCount} image
|
|
||||||
{completedCount !== 1 ? 's' : ''} generated successfully
|
|
||||||
{failedCount > 0 && `, ${failedCount} failed`}.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isProcessing}
|
||||||
|
title={isProcessing ? 'Please wait for generation to complete' : 'Close'}
|
||||||
|
>
|
||||||
|
<XIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Queue List */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{localQueue.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.index}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-colors ${
|
||||||
|
item.status === 'processing'
|
||||||
|
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
|
||||||
|
: item.status === 'completed'
|
||||||
|
? 'bg-green-50 dark:bg-green-900/20 border-green-500'
|
||||||
|
: item.status === 'failed'
|
||||||
|
? 'bg-red-50 dark:bg-red-900/20 border-red-500'
|
||||||
|
: 'bg-gray-50 dark:bg-gray-700/50 border-gray-300 dark:border-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Left: Queue Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
{/* Header Row */}
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-500 text-white text-xs font-bold">
|
||||||
|
{item.index}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-sm text-gray-900 dark:text-white">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
|
{item.contentTitle}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-semibold text-gray-600 dark:text-gray-300 whitespace-nowrap">
|
||||||
|
{getStatusIcon(item.status)} {getStatusText(item.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="relative h-5 bg-gray-200 dark:bg-gray-600 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${getProgressColor(item.status)} transition-all duration-300 ease-out`}
|
||||||
|
style={{ width: `${item.progress}%` }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-xs font-bold text-gray-700 dark:text-gray-200">
|
||||||
|
{item.progress}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{item.error && (
|
||||||
|
<div className="mt-2 p-2 bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 rounded text-xs text-red-700 dark:text-red-300">
|
||||||
|
{item.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Thumbnail */}
|
||||||
|
<div className="w-20 h-20 bg-gray-100 dark:bg-gray-700 rounded-lg overflow-hidden flex-shrink-0 flex items-center justify-center">
|
||||||
|
{item.imageUrl ? (
|
||||||
|
<img
|
||||||
|
src={item.imageUrl}
|
||||||
|
alt={item.label}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
No image
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{completedCount} completed{failedCount > 0 ? `, ${failedCount} failed` : ''} of {totalImages} total
|
||||||
|
</div>
|
||||||
|
{allDone && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import {
|
|||||||
ContentImagesGroup,
|
ContentImagesGroup,
|
||||||
ContentImagesResponse,
|
ContentImagesResponse,
|
||||||
generateImages,
|
generateImages,
|
||||||
|
fetchImageGenerationSettings,
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { FileIcon, DownloadIcon, BoltIcon } from '../../icons';
|
import { FileIcon, DownloadIcon, BoltIcon } from '../../icons';
|
||||||
import { createImagesPageConfig } from '../../config/pages/images.config';
|
import { createImagesPageConfig } from '../../config/pages/images.config';
|
||||||
|
import ImageQueueModal, { ImageQueueItem } from '../../components/common/ImageQueueModal';
|
||||||
|
|
||||||
export default function Images() {
|
export default function Images() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -38,6 +40,11 @@ export default function Images() {
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
|
// Image queue modal state
|
||||||
|
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
|
||||||
|
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
|
||||||
|
const [currentContentId, setCurrentContentId] = useState<number | null>(null);
|
||||||
|
|
||||||
// Load images - wrapped in useCallback
|
// Load images - wrapped in useCallback
|
||||||
const loadImages = useCallback(async () => {
|
const loadImages = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -137,87 +144,105 @@ export default function Images() {
|
|||||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
// Generate images handler
|
// Build image queue structure
|
||||||
|
const buildImageQueue = useCallback((contentId: number, maxInArticleImages: number) => {
|
||||||
|
const contentImages = images.find(g => g.content_id === contentId);
|
||||||
|
if (!contentImages) return [];
|
||||||
|
|
||||||
|
const queue: ImageQueueItem[] = [];
|
||||||
|
let queueIndex = 1;
|
||||||
|
|
||||||
|
// Featured image (always first)
|
||||||
|
if (contentImages.featured_image?.status === 'pending' &&
|
||||||
|
contentImages.featured_image?.prompt) {
|
||||||
|
queue.push({
|
||||||
|
imageId: contentImages.featured_image.id || null,
|
||||||
|
index: queueIndex++,
|
||||||
|
label: 'Featured Image',
|
||||||
|
type: 'featured',
|
||||||
|
contentTitle: contentImages.content_title || `Content #${contentId}`,
|
||||||
|
status: 'pending',
|
||||||
|
progress: 0,
|
||||||
|
imageUrl: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-article images (up to max_in_article_images)
|
||||||
|
const pendingInArticle = contentImages.in_article_images
|
||||||
|
.filter(img => img.status === 'pending' && img.prompt)
|
||||||
|
.slice(0, maxInArticleImages)
|
||||||
|
.sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||||
|
|
||||||
|
pendingInArticle.forEach((img, idx) => {
|
||||||
|
queue.push({
|
||||||
|
imageId: img.id || null,
|
||||||
|
index: queueIndex++,
|
||||||
|
label: `In-Article Image ${img.position || idx + 1}`,
|
||||||
|
type: 'in_article',
|
||||||
|
position: img.position || idx + 1,
|
||||||
|
contentTitle: contentImages.content_title || `Content #${contentId}`,
|
||||||
|
status: 'pending',
|
||||||
|
progress: 0,
|
||||||
|
imageUrl: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return queue;
|
||||||
|
}, [images]);
|
||||||
|
|
||||||
|
// Generate images handler - Stage 1: Open modal immediately
|
||||||
const handleGenerateImages = useCallback(async (contentId: number) => {
|
const handleGenerateImages = useCallback(async (contentId: number) => {
|
||||||
try {
|
try {
|
||||||
// Get all pending images for this content
|
// Get content images
|
||||||
const contentImages = images.find(g => g.content_id === contentId);
|
const contentImages = images.find(g => g.content_id === contentId);
|
||||||
if (!contentImages) {
|
if (!contentImages) {
|
||||||
toast.error('Content not found');
|
toast.error('Content not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all image IDs with prompts and pending status
|
// Fetch image generation settings to get max_in_article_images
|
||||||
const imageIds: number[] = [];
|
let maxInArticleImages = 2; // Default
|
||||||
if (contentImages.featured_image?.id &&
|
try {
|
||||||
contentImages.featured_image.status === 'pending' &&
|
const settings = await fetchImageGenerationSettings();
|
||||||
contentImages.featured_image.prompt) {
|
if (settings.success && settings.config) {
|
||||||
imageIds.push(contentImages.featured_image.id);
|
maxInArticleImages = settings.config.max_in_article_images || 2;
|
||||||
}
|
|
||||||
contentImages.in_article_images.forEach(img => {
|
|
||||||
if (img.id && img.status === 'pending' && img.prompt) {
|
|
||||||
imageIds.push(img.id);
|
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch image settings, using default:', error);
|
||||||
|
}
|
||||||
|
|
||||||
if (imageIds.length === 0) {
|
// Build image queue
|
||||||
|
const queue = buildImageQueue(contentId, maxInArticleImages);
|
||||||
|
|
||||||
|
if (queue.length === 0) {
|
||||||
toast.info('No pending images with prompts found for this content');
|
toast.info('No pending images with prompts found for this content');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Generate Images] Request:', { imageIds, count: imageIds.length });
|
// STAGE 1: Open modal immediately with all progress bars
|
||||||
console.log('[Generate Images] Endpoint: /v1/writer/images/generate_images/');
|
setImageQueue(queue);
|
||||||
|
setCurrentContentId(contentId);
|
||||||
|
setIsQueueModalOpen(true);
|
||||||
|
|
||||||
const result = await generateImages(imageIds);
|
// Collect image IDs for API call (will be used in Stage 2)
|
||||||
|
const imageIds: number[] = queue
|
||||||
|
.map(item => item.imageId)
|
||||||
|
.filter((id): id is number => id !== null);
|
||||||
|
|
||||||
console.log('[Generate Images] Full Response:', result);
|
console.log('[Generate Images] Stage 1 Complete: Modal opened with', queue.length, 'images');
|
||||||
console.log('[Generate Images] Response Keys:', Object.keys(result));
|
console.log('[Generate Images] Image IDs to generate:', imageIds);
|
||||||
|
console.log('[Generate Images] Max in-article images from settings:', maxInArticleImages);
|
||||||
|
|
||||||
|
// TODO: Stage 2 - Start actual generation
|
||||||
|
// This will be implemented in Stage 2
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Log queued prompts if available (TEST MODE)
|
|
||||||
if (result.queued_prompts && result.queued_prompts.length > 0) {
|
|
||||||
console.log('[Generate Images] Queued Prompts (TEST MODE - NOT sent to AI):', result.queued_prompts);
|
|
||||||
console.log(`[Generate Images] Provider: ${result.provider}, Model: ${result.model}`);
|
|
||||||
result.queued_prompts.forEach((qp: any, idx: number) => {
|
|
||||||
console.log(`[Generate Images] Prompt ${idx + 1}/${result.queued_prompts.length}:`);
|
|
||||||
console.log(` - Image Type: ${qp.image_type}`);
|
|
||||||
console.log(` - Content: ${qp.content_title}`);
|
|
||||||
console.log(` - Prompt Length: ${qp.prompt_length} chars`);
|
|
||||||
console.log(` - Full Prompt:`, qp.formatted_prompt);
|
|
||||||
if (qp.negative_prompt) {
|
|
||||||
console.log(` - Negative Prompt:`, qp.negative_prompt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show toast message (no progress modal)
|
|
||||||
const generated = result.images_generated || 0;
|
|
||||||
const failed = result.images_failed || 0;
|
|
||||||
if (generated > 0) {
|
|
||||||
toast.success(`Images generated: ${generated} image${generated !== 1 ? 's' : ''} created${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
||||||
} else if (failed > 0) {
|
|
||||||
toast.error(`Image generation failed: ${failed} image${failed !== 1 ? 's' : ''} failed`);
|
|
||||||
} else {
|
|
||||||
toast.success('Image generation completed');
|
|
||||||
}
|
|
||||||
loadImages(); // Reload to show new images
|
|
||||||
} else {
|
|
||||||
console.error('[Generate Images] Error:', result.error);
|
|
||||||
console.error('[Generate Images] Full Error Response:', result);
|
|
||||||
toast.error(result.error || 'Failed to generate images');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Generate Images] Exception:', error);
|
console.error('[Generate Images] Exception:', error);
|
||||||
console.error('[Generate Images] Error Details:', {
|
toast.error(`Failed to initialize image generation: ${error.message}`);
|
||||||
message: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
response: error.response,
|
|
||||||
status: error.status,
|
|
||||||
statusText: error.statusText
|
|
||||||
});
|
|
||||||
toast.error(`Failed to generate images: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}, [toast, loadImages, images]);
|
}, [toast, images, buildImageQueue]);
|
||||||
|
|
||||||
// Get max in-article images from the data (to determine column count)
|
// Get max in-article images from the data (to determine column count)
|
||||||
const maxInArticleImages = useMemo(() => {
|
const maxInArticleImages = useMemo(() => {
|
||||||
@@ -303,6 +328,19 @@ export default function Images() {
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ImageQueueModal
|
||||||
|
isOpen={isQueueModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsQueueModalOpen(false);
|
||||||
|
setImageQueue([]);
|
||||||
|
setCurrentContentId(null);
|
||||||
|
// Reload images after closing if generation completed
|
||||||
|
loadImages();
|
||||||
|
}}
|
||||||
|
queue={imageQueue}
|
||||||
|
totalImages={imageQueue.length}
|
||||||
|
onUpdateQueue={setImageQueue}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1039,6 +1039,23 @@ export async function generateImages(imageIds: number[]): Promise<any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageGenerationSettings {
|
||||||
|
success: boolean;
|
||||||
|
config: {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
image_type: string;
|
||||||
|
max_in_article_images: number;
|
||||||
|
image_format: string;
|
||||||
|
desktop_enabled: boolean;
|
||||||
|
mobile_enabled: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchImageGenerationSettings(): Promise<ImageGenerationSettings> {
|
||||||
|
return fetchAPI('/v1/system/integrations/image_generation/');
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteTaskImage(id: number): Promise<void> {
|
export async function deleteTaskImage(id: number): Promise<void> {
|
||||||
return fetchAPI(`/v1/writer/images/${id}/`, {
|
return fetchAPI(`/v1/writer/images/${id}/`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|||||||
Reference in New Issue
Block a user