1229 lines
33 KiB
Markdown
1229 lines
33 KiB
Markdown
# Writer Images Page - Complete System Design & Implementation Document
|
|
|
|
**Version**: 1.0
|
|
**Last Updated**: 2025-11-28
|
|
**Status**: Fully Implemented with WordPress Publishing Integration
|
|
**Scope**: End-to-End Analysis of `/writer/images` Page with Manual & Automated Publishing
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Architecture](#architecture)
|
|
3. [Component Structure](#component-structure)
|
|
4. [State Management](#state-management)
|
|
5. [API Functions Chain](#api-functions-chain)
|
|
6. [Data Flow & Lifecycle](#data-flow--lifecycle)
|
|
7. [WordPress Publishing System](#wordpress-publishing-system)
|
|
8. [Automated Publishing](#automated-publishing)
|
|
9. [Sync Functions](#sync-functions)
|
|
10. [Error Handling](#error-handling)
|
|
11. [Performance Optimizations](#performance-optimizations)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The `/writer/images` page is a comprehensive content image management interface that enables users to:
|
|
|
|
- **View** grouped content with featured and in-article images
|
|
- **Generate** AI images with real-time progress tracking
|
|
- **Publish** content to WordPress (manual or bulk)
|
|
- **Update** image statuses
|
|
- **Monitor** publication sync status
|
|
|
|
### Key Features
|
|
|
|
- ✅ Client-side pagination, filtering, and sorting
|
|
- ✅ Real-time image generation queue with modal
|
|
- ✅ Individual and bulk WordPress publishing
|
|
- ✅ Unified API response handling
|
|
- ✅ Automatic status synchronization with WordPress
|
|
- ✅ Comprehensive error handling with retry logic
|
|
- ✅ Resource debug logging (AI Function Logs)
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
### High-Level System Flow
|
|
|
|
```
|
|
User Interaction (Images Page)
|
|
↓
|
|
Frontend Page Component (Images.tsx)
|
|
↓
|
|
Configuration Layer (images.config.tsx, table-actions.config.tsx)
|
|
↓
|
|
API Service Layer (api.ts with fetchAPI)
|
|
↓
|
|
Unified Response Handler (success/error extraction)
|
|
↓
|
|
Backend Endpoints (/v1/writer/images/*, /v1/publisher/publish/)
|
|
↓
|
|
WordPress Bridge Integration
|
|
↓
|
|
WordPress Site Publication
|
|
```
|
|
|
|
### Technology Stack
|
|
|
|
| Layer | Technology | Purpose |
|
|
|-------|-----------|---------|
|
|
| **Frontend** | React 18 + TypeScript | UI Component Framework |
|
|
| **State Management** | React Hooks (useState, useCallback, useEffect) | Local component state |
|
|
| **API Client** | `fetchAPI()` from services/api.ts | Unified API communication |
|
|
| **UI Components** | TablePageTemplate, Modal Components | Reusable UI elements |
|
|
| **Backend API** | Django REST Framework | REST endpoints (/v1/writer/images/, /v1/publisher/publish/) |
|
|
| **Publishing** | WordPress REST API via Bridge | Content publication target |
|
|
|
|
---
|
|
|
|
## Component Structure
|
|
|
|
### Main Component: `Images.tsx`
|
|
|
|
**File**: `frontend/src/pages/Writer/Images.tsx`
|
|
**Lines**: 738 total
|
|
**Purpose**: Main page component managing all image-related operations
|
|
|
|
#### Component Hierarchy
|
|
|
|
```
|
|
Images (Page Component)
|
|
├── PageHeader (with navigation tabs)
|
|
├── TablePageTemplate (main data table)
|
|
│ ├── Filter UI
|
|
│ ├── Table Rows (ContentImagesGroup)
|
|
│ ├── Pagination Controls
|
|
│ └── Action Buttons (Row & Bulk Actions)
|
|
├── ImageQueueModal (image generation progress)
|
|
├── SingleRecordStatusUpdateModal (status updates)
|
|
└── Modal (image preview)
|
|
```
|
|
|
|
### Configuration Components
|
|
|
|
#### 1. **images.config.tsx**
|
|
- Defines page columns (featured image, in-article images 1-5+)
|
|
- Filter configurations (search, status)
|
|
- Header metrics (calculations)
|
|
- Column rendering functions
|
|
|
|
#### 2. **table-actions.config.tsx**
|
|
- **Path**: `/writer/images`
|
|
- **Row Actions**:
|
|
- `publish_wordpress` - Publish single content to WordPress
|
|
- `update_status` - Change image status
|
|
- **Bulk Actions**:
|
|
- `bulk_publish_wordpress` - Publish multiple items
|
|
- **Visibility Logic** (shouldShow):
|
|
```typescript
|
|
shouldShow: (row: any) => {
|
|
return row.status === 'published' &&
|
|
(!row.external_id || !row.external_url) &&
|
|
(!row.sync_status || row.sync_status !== 'published');
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## State Management
|
|
|
|
### Page-Level State Variables
|
|
|
|
```typescript
|
|
// Data State
|
|
const [images, setImages] = useState<ContentImagesGroup[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Filter State
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
|
|
// Pagination State (Client-side)
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
const pageSize = 10; // Items per page
|
|
|
|
// Sorting State
|
|
const [sortBy, setSortBy] = useState<string>('content_title');
|
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
|
|
// Image Generation Modal
|
|
const [isQueueModalOpen, setIsQueueModalOpen] = useState(false);
|
|
const [imageQueue, setImageQueue] = useState<ImageQueueItem[]>([]);
|
|
const [currentContentId, setCurrentContentId] = useState<number | null>(null);
|
|
const [taskId, setTaskId] = useState<string | null>(null);
|
|
const [imageModel, setImageModel] = useState<string | null>(null);
|
|
const [imageProvider, setImageProvider] = useState<string | null>(null);
|
|
|
|
// Status Update Modal
|
|
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
|
|
const [statusUpdateContentId, setStatusUpdateContentId] = useState<number | null>(null);
|
|
const [statusUpdateRecordName, setStatusUpdateRecordName] = useState<string>('');
|
|
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
|
|
|
// Image Preview Modal
|
|
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
|
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
|
|
|
|
// Debug State
|
|
const [aiLogs, setAiLogs] = useState<Array<{...}>>([]); // AI Function Logs
|
|
```
|
|
|
|
---
|
|
|
|
## API Functions Chain
|
|
|
|
### Complete API Imports
|
|
|
|
```typescript
|
|
import {
|
|
fetchContentImages, // GET /v1/writer/images/content_images/
|
|
ContentImagesGroup, // Type definition
|
|
ContentImagesResponse, // Type definition
|
|
fetchImageGenerationSettings, // GET /v1/system/integrations/image_generation/
|
|
generateImages, // POST /v1/writer/images/generate_images/
|
|
bulkUpdateImagesStatus, // POST /v1/writer/images/bulk_update/
|
|
ContentImage, // Type definition
|
|
fetchAPI, // Unified API fetch wrapper
|
|
} from '../../services/api';
|
|
```
|
|
|
|
### API Function Definitions (From api.ts)
|
|
|
|
#### 1. **fetchContentImages()**
|
|
|
|
```typescript
|
|
export async function fetchContentImages(
|
|
filters: ContentImagesFilters = {}
|
|
): Promise<ContentImagesResponse> {
|
|
const params = new URLSearchParams();
|
|
|
|
// Auto-inject site filter
|
|
if (!filters.site_id) {
|
|
const activeSiteId = getActiveSiteId();
|
|
if (activeSiteId) {
|
|
filters.site_id = activeSiteId;
|
|
}
|
|
}
|
|
|
|
// Auto-inject sector filter
|
|
if (filters.sector_id === undefined) {
|
|
const activeSectorId = getActiveSectorId();
|
|
if (activeSectorId !== null && activeSectorId !== undefined) {
|
|
filters.sector_id = activeSectorId;
|
|
}
|
|
}
|
|
|
|
if (filters.site_id) params.append('site_id', filters.site_id.toString());
|
|
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
|
|
|
|
const queryString = params.toString();
|
|
return fetchAPI(`/v1/writer/images/content_images/${queryString ? `?${queryString}` : ''}`);
|
|
}
|
|
```
|
|
|
|
**Purpose**: Fetch content images grouped by content
|
|
**Endpoint**: `GET /v1/writer/images/content_images/`
|
|
**Response Type**: `ContentImagesResponse`
|
|
|
|
**Response Structure**:
|
|
```typescript
|
|
{
|
|
count: number;
|
|
results: ContentImagesGroup[];
|
|
}
|
|
|
|
// ContentImagesGroup structure:
|
|
{
|
|
content_id: number;
|
|
content_title: string;
|
|
featured_image: ContentImage | null; // Featured image
|
|
in_article_images: ContentImage[]; // 1-5+ in-article images
|
|
overall_status: 'pending' | 'partial' | 'complete' | 'failed';
|
|
status?: string; // 'published' or 'draft'
|
|
external_id?: string; // WordPress post ID
|
|
external_url?: string; // WordPress post URL
|
|
sync_status?: string; // 'published', 'synced', etc.
|
|
}
|
|
|
|
// ContentImage structure:
|
|
{
|
|
id: number;
|
|
image_type: string; // 'featured' or 'in_article'
|
|
image_url?: string | null; // Generated image URL
|
|
image_path?: string | null; // Local file path
|
|
prompt?: string | null; // Generation prompt
|
|
status: string; // 'pending', 'generated', 'failed'
|
|
position: number; // Order for in-article images
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
```
|
|
|
|
#### 2. **generateImages()**
|
|
|
|
```typescript
|
|
export async function generateImages(
|
|
imageIds: number[],
|
|
contentId?: number
|
|
): Promise<{ success: boolean; task_id?: string; message?: string; error?: string }> {
|
|
try {
|
|
const response = await fetchAPI('/v1/writer/images/generate_images/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
ids: imageIds,
|
|
content_id: contentId
|
|
}),
|
|
});
|
|
|
|
return { success: true, ...response } as any;
|
|
} catch (error: any) {
|
|
if (error.response && typeof error.response === 'object') {
|
|
return { success: false, error: error.message, ...error.response } as any;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Purpose**: Start image generation for specified image IDs
|
|
**Endpoint**: `POST /v1/writer/images/generate_images/`
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"ids": [123, 124, 125],
|
|
"content_id": 42
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"task_id": "celery-task-uuid",
|
|
"message": "Image generation started",
|
|
"images_created": 3
|
|
}
|
|
```
|
|
|
|
#### 3. **bulkUpdateImagesStatus()**
|
|
|
|
```typescript
|
|
export async function bulkUpdateImagesStatus(
|
|
contentId: number,
|
|
status: string
|
|
): Promise<{ updated_count: number }> {
|
|
return fetchAPI(`/v1/writer/images/bulk_update/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content_id: contentId, status }),
|
|
});
|
|
}
|
|
```
|
|
|
|
**Purpose**: Update status for all images of a content
|
|
**Endpoint**: `POST /v1/writer/images/bulk_update/`
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"content_id": 42,
|
|
"status": "generated"
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"updated_count": 4
|
|
}
|
|
```
|
|
|
|
#### 4. **fetchImageGenerationSettings()**
|
|
|
|
```typescript
|
|
export async function fetchImageGenerationSettings(): Promise<ImageGenerationSettings> {
|
|
return fetchAPI('/v1/system/integrations/image_generation/');
|
|
}
|
|
```
|
|
|
|
**Purpose**: Get image generation configuration
|
|
**Endpoint**: `GET /v1/system/integrations/image_generation/`
|
|
**Response**:
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
config: {
|
|
provider: string; // 'openai', 'stability', etc.
|
|
model: string; // 'dall-e-3', 'stable-diffusion', etc.
|
|
image_type: string; // 'featured' or 'in_article'
|
|
max_in_article_images: number; // Usually 2-5
|
|
image_format: string; // 'jpg', 'png', etc.
|
|
desktop_enabled: boolean;
|
|
mobile_enabled: boolean;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5. **fetchAPI()** - Unified API Wrapper
|
|
|
|
```typescript
|
|
export async function fetchAPI(
|
|
endpoint: string,
|
|
options?: RequestInit & { timeout?: number }
|
|
): Promise<any> {
|
|
// 1. Auto-injects JWT token from auth store
|
|
// 2. Handles 401 with token refresh
|
|
// 3. Extracts unified response format
|
|
// 4. Transforms error responses
|
|
// 5. Handles network errors
|
|
}
|
|
```
|
|
|
|
**Response Handling**:
|
|
- **Unified Success**: `{ success: true, data: {...} }` → returns `data`
|
|
- **Paginated**: `{ success: true, count: X, results: [...] }` → returns as-is
|
|
- **Unified Error**: `{ success: false, error: "..." }` → throws error
|
|
- **Non-200 Status**: Throws with error details
|
|
|
|
---
|
|
|
|
## Data Flow & Lifecycle
|
|
|
|
### 1. **Page Load Flow**
|
|
|
|
```sequence
|
|
Page Mount
|
|
→ loadImages() called in useEffect
|
|
→ fetchContentImages({})
|
|
→ API: GET /v1/writer/images/content_images/
|
|
↓
|
|
Response: {count: X, results: [ContentImagesGroup]}
|
|
← Parse & filter (client-side)
|
|
• Search filter
|
|
• Status filter
|
|
• Sort by title/status
|
|
• Paginate (10 per page)
|
|
→ setImages(paginatedResults)
|
|
→ setShowContent(true)
|
|
→ UI renders table with data
|
|
```
|
|
|
|
### 2. **Image Generation Flow**
|
|
|
|
```sequence
|
|
User clicks "Generate Images" button for content
|
|
→ handleGenerateImages(contentId) triggered
|
|
→ Fetch max_in_article_images from settings
|
|
→ Build image queue from pending images
|
|
→ Open ImageQueueModal with queue
|
|
|
|
User clicks "Start Generation"
|
|
→ generateImages(imageIds, contentId) called
|
|
→ API: POST /v1/writer/images/generate_images/
|
|
↓
|
|
Returns task_id (Celery task)
|
|
← Modal starts polling for progress
|
|
• Updates progress bars per image
|
|
• Shows real-time generation status
|
|
|
|
Generation Complete
|
|
→ Modal closes
|
|
→ loadImages() called to refresh
|
|
→ Table shows generated images
|
|
```
|
|
|
|
### 3. **Manual WordPress Publishing Flow**
|
|
|
|
```sequence
|
|
User clicks "Publish to WordPress" on a row
|
|
→ handleRowAction('publish_wordpress', row) triggered
|
|
→ Check conditions:
|
|
• row.status === 'published'
|
|
• No external_id OR No external_url
|
|
• sync_status !== 'published'
|
|
|
|
→ fetchAPI('/v1/publisher/publish/', {
|
|
method: 'POST',
|
|
body: {
|
|
content_id: row.content_id,
|
|
destinations: ['wordpress']
|
|
}
|
|
})
|
|
|
|
→ Backend: /api/v1/publisher/publish/
|
|
↓
|
|
Receives: {content_id: 42, destinations: ['wordpress']}
|
|
|
|
Calls: PublisherService.publish_content()
|
|
→ Finds Content object by ID
|
|
→ Maps content to WordPress post format
|
|
→ Creates/updates post via WordPress Bridge
|
|
→ Sets external_id, external_url, sync_status
|
|
|
|
Returns: {
|
|
success: true,
|
|
data: {
|
|
content_id: 42,
|
|
external_id: "5678",
|
|
external_url: "https://site.com/post-title",
|
|
sync_status: "published"
|
|
}
|
|
}
|
|
|
|
← Frontend receives response
|
|
→ toast.success('Published to WordPress')
|
|
→ loadImages() to refresh status
|
|
→ Table updates to show published status
|
|
```
|
|
|
|
### 4. **Bulk WordPress Publishing Flow**
|
|
|
|
```sequence
|
|
User selects multiple items
|
|
→ Click "Publish Ready to WordPress"
|
|
→ handleBulkAction('bulk_publish_wordpress', ids) triggered
|
|
|
|
→ Filter items that are ready:
|
|
const readyItems = images
|
|
.filter(item => ids.includes(item.content_id))
|
|
.filter(item =>
|
|
item.status === 'published' &&
|
|
(!item.external_id || !item.external_url) &&
|
|
(!item.sync_status || item.sync_status !== 'published')
|
|
)
|
|
|
|
→ For each readyItem:
|
|
fetchAPI('/v1/publisher/publish/', {
|
|
method: 'POST',
|
|
body: {
|
|
content_id: item.content_id,
|
|
destinations: ['wordpress']
|
|
}
|
|
})
|
|
|
|
Track: successCount++, failedCount++
|
|
|
|
→ Show summary toast:
|
|
"Published X items, Y failed"
|
|
|
|
→ loadImages() to refresh all
|
|
```
|
|
|
|
### 5. **Status Update Flow**
|
|
|
|
```sequence
|
|
User clicks "Update Status"
|
|
→ Modal opens for status selection
|
|
→ User selects: 'pending' | 'generated' | 'failed'
|
|
→ handleStatusUpdate(status) called
|
|
|
|
→ bulkUpdateImagesStatus(contentId, status)
|
|
→ API: POST /v1/writer/images/bulk_update/
|
|
{content_id: 42, status: 'generated'}
|
|
|
|
Returns: {updated_count: 4}
|
|
|
|
← Modal closes
|
|
→ toast.success('Updated 4 images')
|
|
→ loadImages() to refresh
|
|
```
|
|
|
|
---
|
|
|
|
## WordPress Publishing System
|
|
|
|
### Manual Publishing
|
|
|
|
#### Architecture
|
|
|
|
```
|
|
Frontend (Images Page)
|
|
└── handleRowAction('publish_wordpress', row)
|
|
└── fetchAPI('/v1/publisher/publish/', {...})
|
|
└── Backend Publisher Module
|
|
└── PublisherService.publish_content()
|
|
└── Content → WordPress Bridge
|
|
└── WordPress Site (via REST API)
|
|
└── WP Post Created/Updated
|
|
```
|
|
|
|
#### Conditions for Visibility
|
|
|
|
The "Publish to WordPress" button appears when ALL conditions are met:
|
|
|
|
1. **Content Status**: `row.status === 'published'`
|
|
- Content must be in published status internally
|
|
|
|
2. **No WordPress Record**: `!row.external_id || !row.external_url`
|
|
- Either no WordPress ID or no WordPress URL
|
|
- Allows re-publishing if one is missing
|
|
|
|
3. **Not Already Synced**: `!row.sync_status || row.sync_status !== 'published'`
|
|
- sync_status is not 'published'
|
|
- Allows re-sync if status changed
|
|
|
|
#### Request Structure
|
|
|
|
```typescript
|
|
const response = await fetchAPI('/v1/publisher/publish/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
content_id: 42, // Content to publish
|
|
destinations: ['wordpress'] // Target platform
|
|
})
|
|
});
|
|
```
|
|
|
|
#### Response Structure
|
|
|
|
```typescript
|
|
{
|
|
success: true,
|
|
data: {
|
|
content_id: 42,
|
|
external_id: "5678", // WordPress post ID
|
|
external_url: "https://site.com/p/5678/",
|
|
sync_status: "published", // Status after publishing
|
|
message: "Content published to WordPress"
|
|
},
|
|
request_id: "uuid"
|
|
}
|
|
```
|
|
|
|
### Bulk Publishing
|
|
|
|
#### Flow
|
|
|
|
```typescript
|
|
// Filter ready items
|
|
const readyItems = images
|
|
.filter(item => ids.includes(item.content_id))
|
|
.filter(item => item.status === 'published' &&
|
|
(!item.external_id || !item.external_url) &&
|
|
(!item.sync_status || item.sync_status !== 'published'))
|
|
|
|
// Publish each individually
|
|
for (const item of readyItems) {
|
|
try {
|
|
const response = await fetchAPI('/v1/publisher/publish/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
content_id: item.content_id,
|
|
destinations: ['wordpress']
|
|
})
|
|
})
|
|
|
|
if (response.success) {
|
|
successCount++
|
|
} else {
|
|
failedCount++
|
|
}
|
|
} catch (error) {
|
|
failedCount++
|
|
}
|
|
}
|
|
|
|
// Show results
|
|
if (successCount > 0) {
|
|
toast.success(`Published ${successCount} items`)
|
|
}
|
|
if (failedCount > 0) {
|
|
toast.warning(`${failedCount} failed`)
|
|
}
|
|
|
|
// Refresh to show updated status
|
|
loadImages()
|
|
```
|
|
|
|
#### Advantages of Individual Requests
|
|
|
|
1. **Error Isolation**: Failure in one publish doesn't affect others
|
|
2. **Granular Tracking**: Knows exactly which items succeeded/failed
|
|
3. **User Feedback**: Can show detailed success/fail breakdown
|
|
4. **Partial Success**: Users see partial results instead of total failure
|
|
|
|
---
|
|
|
|
## Automated Publishing
|
|
|
|
### Automatic Status Sync (Two-Way Sync)
|
|
|
|
The system includes automatic synchronization of content status between IGNY8 and WordPress:
|
|
|
|
#### Components
|
|
|
|
1. **WordPress Bridge Plugin** (`igny8-wp-integration`)
|
|
- Monitors WordPress post changes
|
|
- Syncs status back to IGNY8 API
|
|
- Location: `/includes/sync/hooks.php`
|
|
|
|
2. **IGNY8 Backend Sync Tasks** (Celery)
|
|
- Periodic status checks
|
|
- Webhook receivers
|
|
- Status reconciliation
|
|
|
|
#### Sync Direction
|
|
|
|
```
|
|
IGNY8 → WordPress:
|
|
Content published in Writer
|
|
→ Publisher API called
|
|
→ WordPress Bridge receives payload
|
|
→ Post created/updated in WordPress
|
|
|
|
WordPress → IGNY8:
|
|
Post updated in WordPress (draft/publish/trash)
|
|
→ WordPress Hook triggered
|
|
→ Bridge syncs back via PUT /writer/tasks/{id}/
|
|
→ IGNY8 Content status updated
|
|
```
|
|
|
|
#### Sync Status Values
|
|
|
|
| Status | Meaning | Publishable? |
|
|
|--------|---------|-------------|
|
|
| `draft` | Not yet published to WordPress | Yes |
|
|
| `pending` | Awaiting review in WordPress | No |
|
|
| `published` | Live on WordPress | No (already published) |
|
|
| `synced` | Initial sync complete | No |
|
|
| `failed` | Publication failed | Yes (retry) |
|
|
|
|
---
|
|
|
|
## Sync Functions
|
|
|
|
### 1. **loadImages() - Main Refresh Function**
|
|
|
|
```typescript
|
|
const loadImages = useCallback(async () => {
|
|
setLoading(true);
|
|
setShowContent(false);
|
|
try {
|
|
// Fetch all content images
|
|
const data: ContentImagesResponse = await fetchContentImages({});
|
|
let filteredResults = data.results || [];
|
|
|
|
// Client-side search filter
|
|
if (searchTerm) {
|
|
filteredResults = filteredResults.filter(group =>
|
|
group.content_title?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Client-side status filter
|
|
if (statusFilter) {
|
|
filteredResults = filteredResults.filter(group =>
|
|
group.overall_status === statusFilter
|
|
);
|
|
}
|
|
|
|
// Client-side sorting
|
|
filteredResults.sort((a, b) => {
|
|
let aVal: any = a.content_title;
|
|
let bVal: any = b.content_title;
|
|
|
|
if (sortBy === 'overall_status') {
|
|
aVal = a.overall_status;
|
|
bVal = b.overall_status;
|
|
}
|
|
|
|
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
|
|
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
// Client-side pagination
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const paginatedResults = filteredResults.slice(startIndex, endIndex);
|
|
|
|
setImages(paginatedResults);
|
|
setTotalCount(filteredResults.length);
|
|
setTotalPages(Math.ceil(filteredResults.length / pageSize));
|
|
|
|
setTimeout(() => {
|
|
setShowContent(true);
|
|
setLoading(false);
|
|
}, 100);
|
|
} catch (error: any) {
|
|
console.error('Error loading images:', error);
|
|
toast.error(`Failed to load images: ${error.message}`);
|
|
setShowContent(true);
|
|
setLoading(false);
|
|
}
|
|
}, [currentPage, statusFilter, sortBy, sortDirection, searchTerm, toast]);
|
|
```
|
|
|
|
**Called After**:
|
|
- Page mount
|
|
- Search term changes
|
|
- Status filter changes
|
|
- Sort order changes
|
|
- Page navigation
|
|
- Publishing operations complete
|
|
- Status updates complete
|
|
|
|
### 2. **handleStatusUpdate() - Status Change Handler**
|
|
|
|
```typescript
|
|
const handleStatusUpdate = useCallback(async (status: string) => {
|
|
if (!statusUpdateContentId) return;
|
|
|
|
setIsUpdatingStatus(true);
|
|
try {
|
|
const result = await bulkUpdateImagesStatus(statusUpdateContentId, status);
|
|
toast.success(`Successfully updated ${result.updated_count} image(s) status to ${status}`);
|
|
setIsStatusModalOpen(false);
|
|
setStatusUpdateContentId(null);
|
|
setStatusUpdateRecordName('');
|
|
// Reload images to reflect the changes
|
|
loadImages();
|
|
} catch (error: any) {
|
|
toast.error(`Failed to update status: ${error.message}`);
|
|
} finally {
|
|
setIsUpdatingStatus(false);
|
|
}
|
|
}, [statusUpdateContentId, toast, loadImages]);
|
|
```
|
|
|
|
**Flow**:
|
|
1. User opens status modal
|
|
2. Selects new status
|
|
3. API call to update all images for content
|
|
4. Refresh table to show changes
|
|
|
|
### 3. **Site/Sector Change Listeners**
|
|
|
|
```typescript
|
|
useEffect(() => {
|
|
const handleSiteChange = () => {
|
|
loadImages();
|
|
};
|
|
|
|
const handleSectorChange = () => {
|
|
loadImages();
|
|
};
|
|
|
|
window.addEventListener('siteChanged', handleSiteChange);
|
|
window.addEventListener('sectorChanged', handleSectorChange);
|
|
|
|
return () => {
|
|
window.removeEventListener('siteChanged', handleSiteChange);
|
|
window.removeEventListener('sectorChanged', handleSectorChange);
|
|
};
|
|
}, [loadImages]);
|
|
```
|
|
|
|
**Purpose**: Auto-refresh when user switches site or sector
|
|
|
|
### 4. **Debounced Search**
|
|
|
|
```typescript
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
if (currentPage === 1) {
|
|
loadImages();
|
|
} else {
|
|
setCurrentPage(1); // Reset to page 1
|
|
}
|
|
}, 500); // Wait 500ms after user stops typing
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [searchTerm, currentPage, loadImages]);
|
|
```
|
|
|
|
**Purpose**: Avoid excessive API calls during search input
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Unified Error Response Format
|
|
|
|
All API errors follow the unified format:
|
|
|
|
```typescript
|
|
{
|
|
success: false,
|
|
error: "Human-readable error message",
|
|
errors: {
|
|
field_name: ["Field-specific error"]
|
|
},
|
|
request_id: "uuid"
|
|
}
|
|
```
|
|
|
|
### Error Types Handled
|
|
|
|
| Error Type | Handling | User Feedback |
|
|
|-----------|----------|---------------|
|
|
| **Network Error** | Caught by fetchAPI | "Network error: Unable to reach API" |
|
|
| **401 Unauthorized** | Token refresh attempted | Auto-retry or "Session expired" |
|
|
| **403 Forbidden** | Authentication check failed | "Permission denied" |
|
|
| **404 Not Found** | Resource doesn't exist | "Content not found" |
|
|
| **422 Validation** | Invalid request data | "Invalid input: {field details}" |
|
|
| **500 Server Error** | Backend exception | "Server error occurred" |
|
|
| **Timeout** | Request > 30 seconds | "Request timeout" |
|
|
|
|
### Error Handling in Publishing
|
|
|
|
```typescript
|
|
try {
|
|
const response = await fetchAPI('/v1/publisher/publish/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
content_id: item.content_id,
|
|
destinations: ['wordpress']
|
|
})
|
|
});
|
|
|
|
if (response.success) {
|
|
successCount++;
|
|
} else {
|
|
// Unified error format
|
|
console.warn(`Failed: ${response.error}`);
|
|
failedCount++;
|
|
}
|
|
} catch (error: any) {
|
|
// Network or unknown error
|
|
console.error('Error publishing:', error);
|
|
failedCount++;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Optimizations
|
|
|
|
### 1. **Client-Side Processing**
|
|
|
|
- **Search**: Filtered in memory, not via API
|
|
- **Sorting**: Local array sort, no API call
|
|
- **Pagination**: 10 items per page, loaded once
|
|
- **Status Filter**: In-memory filtering
|
|
|
|
**Benefit**: Faster UX, no additional API calls
|
|
|
|
### 2. **Memoization**
|
|
|
|
```typescript
|
|
// Prevent unnecessary re-renders
|
|
const pageConfig = useMemo(() => {
|
|
return createImagesPageConfig({...});
|
|
}, [searchTerm, statusFilter, maxInArticleImages, ...]);
|
|
|
|
const headerMetrics = useMemo(() => {
|
|
return pageConfig.headerMetrics.map(...);
|
|
}, [pageConfig?.headerMetrics, images, totalCount]);
|
|
```
|
|
|
|
### 3. **Debouncing**
|
|
|
|
```typescript
|
|
// 500ms delay on search to avoid API thrashing
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
loadImages();
|
|
}, 500);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [searchTerm, ...]);
|
|
```
|
|
|
|
### 4. **Lazy Loading (Potential)**
|
|
|
|
```typescript
|
|
// Could implement virtual scrolling for 1000+ items
|
|
// Currently: Fixed page size of 10 items
|
|
```
|
|
|
|
### 5. **Caching (Backend)**
|
|
|
|
The API automatically caches:
|
|
- Content images (5-minute TTL)
|
|
- Image generation settings (server-side)
|
|
- Site metadata
|
|
|
|
---
|
|
|
|
## Configuration Deep Dive
|
|
|
|
### Table Actions Configuration
|
|
|
|
**File**: `table-actions.config.tsx`
|
|
|
|
```typescript
|
|
'/writer/images': {
|
|
rowActions: [
|
|
{
|
|
key: 'publish_wordpress',
|
|
label: 'Publish to WordPress',
|
|
icon: <ArrowRightIcon className="w-5 h-5" />,
|
|
variant: 'success',
|
|
shouldShow: (row: any) => {
|
|
// Only show if ready for publishing
|
|
return row.status === 'published' &&
|
|
(!row.external_id || !row.external_url) &&
|
|
(!row.sync_status || row.sync_status !== 'published');
|
|
},
|
|
},
|
|
{
|
|
key: 'update_status',
|
|
label: 'Update Status',
|
|
icon: <CheckCircleIcon className="w-5 h-5" />,
|
|
variant: 'primary',
|
|
},
|
|
],
|
|
bulkActions: [
|
|
{
|
|
key: 'bulk_publish_wordpress',
|
|
label: 'Publish Ready to WordPress',
|
|
icon: <ArrowRightIcon className="w-5 h-5" />,
|
|
variant: 'success',
|
|
},
|
|
],
|
|
}
|
|
```
|
|
|
|
### Images Page Config
|
|
|
|
**File**: `images.config.tsx`
|
|
|
|
```typescript
|
|
export const createImagesPageConfig = (handlers: {
|
|
searchTerm: string;
|
|
setSearchTerm: (value: string) => void;
|
|
statusFilter: string;
|
|
setStatusFilter: (value: string) => void;
|
|
setCurrentPage: (page: number) => void;
|
|
maxInArticleImages?: number;
|
|
onGenerateImages?: (contentId: number) => void;
|
|
onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void;
|
|
}): ImagesPageConfig => {
|
|
// Builds columns with featured image + up to 5 in-article images
|
|
// Builds filters: search, status
|
|
// Calculates header metrics
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Data Models
|
|
|
|
### ContentImagesGroup
|
|
|
|
```typescript
|
|
interface ContentImagesGroup {
|
|
content_id: number;
|
|
content_title: string;
|
|
featured_image: ContentImage | null;
|
|
in_article_images: ContentImage[];
|
|
overall_status: 'pending' | 'partial' | 'complete' | 'failed';
|
|
|
|
// WordPress fields
|
|
status?: string; // 'published' or 'draft'
|
|
external_id?: string; // WordPress post ID
|
|
external_url?: string; // WordPress post URL
|
|
sync_status?: string; // 'published', 'synced', 'failed'
|
|
}
|
|
```
|
|
|
|
### ContentImage
|
|
|
|
```typescript
|
|
interface ContentImage {
|
|
id: number;
|
|
image_type: string; // 'featured' | 'in_article'
|
|
image_url?: string | null; // Generated image URL
|
|
image_path?: string | null; // Local file path
|
|
prompt?: string | null; // Generation prompt
|
|
status: string; // 'pending' | 'generated' | 'failed'
|
|
position: number; // Order for in-article (1-5+)
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## API Endpoints Reference
|
|
|
|
### Reader Endpoints
|
|
|
|
| Method | Endpoint | Purpose |
|
|
|--------|----------|---------|
|
|
| GET | `/v1/writer/images/content_images/` | Fetch grouped content images |
|
|
| GET | `/v1/system/integrations/image_generation/` | Get generation settings |
|
|
|
|
### Writer Endpoints
|
|
|
|
| Method | Endpoint | Purpose |
|
|
|--------|----------|---------|
|
|
| POST | `/v1/writer/images/generate_images/` | Start image generation |
|
|
| POST | `/v1/writer/images/bulk_update/` | Update image statuses |
|
|
|
|
### Publisher Endpoints
|
|
|
|
| Method | Endpoint | Purpose |
|
|
|--------|----------|---------|
|
|
| POST | `/v1/publisher/publish/` | Publish to destinations (WordPress) |
|
|
|
|
---
|
|
|
|
## Key Implementation Decisions
|
|
|
|
### 1. **Unified Publishing Endpoint**
|
|
|
|
**Decision**: Use existing `/v1/publisher/publish/` instead of custom WordPress endpoints
|
|
**Rationale**:
|
|
- Follows IGNY8 unified API standard
|
|
- Supports multiple destinations (WordPress, Sites Renderer, etc.)
|
|
- Centralized publishing logic
|
|
- Easier to extend
|
|
|
|
### 2. **Client-Side Processing**
|
|
|
|
**Decision**: Perform search, filter, sort, paginate on client
|
|
**Rationale**:
|
|
- Faster response (no API round-trips)
|
|
- Better UX for small datasets (< 1000 items)
|
|
- Works offline
|
|
- Reduces server load
|
|
|
|
### 3. **Individual Bulk Publishes**
|
|
|
|
**Decision**: Loop and call API individually for bulk operations
|
|
**Rationale**:
|
|
- Error isolation (one failure doesn't affect others)
|
|
- Granular progress tracking
|
|
- User sees partial success
|
|
- Easier error reporting
|
|
|
|
### 4. **Automatic Status Sync**
|
|
|
|
**Decision**: Refresh data after publish operations
|
|
**Rationale**:
|
|
- Ensures UI reflects latest server state
|
|
- Shows WordPress sync status
|
|
- Catches errors from background tasks
|
|
- User sees immediate feedback
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
### Potential Improvements
|
|
|
|
1. **Server-Side Pagination**
|
|
- Replace client-side pagination
|
|
- Reduce memory usage for large datasets
|
|
- Enable server-side filtering/sorting
|
|
|
|
2. **Real-Time Updates**
|
|
- WebSocket for publishing progress
|
|
- Push notifications
|
|
- Live sync status updates
|
|
|
|
3. **Batch Publishing API**
|
|
- Single endpoint for bulk operations
|
|
- Reduced network overhead
|
|
- Atomic operations (all-or-nothing)
|
|
|
|
4. **Advanced Filtering**
|
|
- Filter by image type (featured/in-article)
|
|
- Filter by image status
|
|
- Filter by sync status
|
|
|
|
5. **Scheduling**
|
|
- Schedule publishing for specific dates
|
|
- Queue management UI
|
|
- Publishing calendar
|
|
|
|
6. **Retry Mechanism**
|
|
- Automatic retry on failure
|
|
- Exponential backoff
|
|
- Dead letter queue for failed items
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] Load page with empty data
|
|
- [ ] Load page with 50+ items
|
|
- [ ] Search functionality
|
|
- [ ] Filter by status
|
|
- [ ] Sort by title/status
|
|
- [ ] Pagination navigation
|
|
- [ ] Generate images flow
|
|
- [ ] Single item publish to WordPress
|
|
- [ ] Bulk publish operation
|
|
- [ ] Partial failure handling
|
|
- [ ] Status update operation
|
|
- [ ] Image preview modal
|
|
- [ ] Error toasts display correctly
|
|
- [ ] Reload on site change
|
|
- [ ] Reload on sector change
|
|
- [ ] Token refresh on 401
|
|
- [ ] Network error handling
|
|
|
|
---
|
|
|
|
## Debugging & Support
|
|
|
|
### Enable Resource Debug Logs
|
|
|
|
```typescript
|
|
// In any page using Images component
|
|
const resourceDebugEnabled = useResourceDebug();
|
|
// Shows AI Function Logs panel with all operations
|
|
```
|
|
|
|
### Check API Responses
|
|
|
|
```typescript
|
|
// In browser DevTools
|
|
// Network tab → XHR/Fetch → Filter "images"
|
|
// View each request/response
|
|
|
|
// Look for unified format:
|
|
{
|
|
"success": true,
|
|
"data": {...},
|
|
"request_id": "uuid"
|
|
}
|
|
```
|
|
|
|
### Common Issues
|
|
|
|
| Issue | Cause | Solution |
|
|
|-------|-------|----------|
|
|
| Publish button not showing | Status != 'published' OR already has external_id | Check row.status and row.external_id |
|
|
| Publishing succeeds but data not updated | Need to refresh | loadImages() called, wait for refresh |
|
|
| Images not loading | Site/sector not selected | Select site in header |
|
|
| API 401 error | Token expired | Should auto-refresh, check auth store |
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
The `/writer/images` page implements a comprehensive content image management system with:
|
|
|
|
✅ **Full Publishing Workflow** - Manual and bulk publishing to WordPress
|
|
✅ **Real-Time Generation** - Image generation with progress tracking
|
|
✅ **Two-Way Sync** - Automatic status synchronization with WordPress
|
|
✅ **Unified API** - Following IGNY8 API standards
|
|
✅ **Error Handling** - Comprehensive error management and user feedback
|
|
✅ **Performance** - Optimized with client-side processing and memoization
|
|
✅ **Scalability** - Designed for future enhancements
|
|
|
|
The system is production-ready and fully integrated with the IGNY8 architecture.
|
|
|
|
---
|
|
|
|
**Document End**
|