AI Image Studio Progress Review

- Added new router for content assets
- Added new service for content assets
- Added new model for content assets
- Added new utils for content assets
- Added new docs for content assets
- Added new tests for content assets
- Added new examples for content assets
- Added new guides for content assets
This commit is contained in:
ajaysi
2025-11-23 09:21:11 +05:30
parent eede21ad42
commit 77d7c0cde6
38 changed files with 5939 additions and 37 deletions

View File

@@ -0,0 +1,244 @@
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '@clerk/clerk-react';
export interface ContentAsset {
id: number;
user_id: string;
asset_type: 'text' | 'image' | 'video' | 'audio';
source_module: string;
filename: string;
file_url: string;
file_path?: string;
file_size?: number;
mime_type?: string;
title?: string;
description?: string;
prompt?: string;
tags: string[];
metadata: Record<string, any>;
provider?: string;
model?: string;
cost: number;
generation_time?: number;
is_favorite: boolean;
download_count: number;
share_count: number;
created_at: string;
updated_at: string;
}
export interface AssetFilters {
asset_type?: 'text' | 'image' | 'video' | 'audio';
source_module?: string;
search?: string;
tags?: string[];
favorites_only?: boolean;
limit?: number;
offset?: number;
}
export interface AssetListResponse {
assets: ContentAsset[];
total: number;
limit: number;
offset: number;
}
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
export const useContentAssets = (filters: AssetFilters = {}) => {
const { getToken } = useAuth();
const [assets, setAssets] = useState<ContentAsset[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [total, setTotal] = useState(0);
const fetchAssets = useCallback(async () => {
try {
setLoading(true);
setError(null);
const token = await getToken();
if (!token) {
throw new Error('Not authenticated');
}
const params = new URLSearchParams();
if (filters.asset_type) params.append('asset_type', filters.asset_type);
if (filters.source_module) params.append('source_module', filters.source_module);
if (filters.search) params.append('search', filters.search);
if (filters.tags && filters.tags.length > 0) params.append('tags', filters.tags.join(','));
if (filters.favorites_only) params.append('favorites_only', 'true');
params.append('limit', String(filters.limit || 100));
params.append('offset', String(filters.offset || 0));
// Add cache busting for fresh data
params.append('_t', String(Date.now()));
const response = await fetch(`${API_BASE_URL}/api/content-assets/?${params.toString()}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch assets: ${response.statusText}`);
}
const data: AssetListResponse = await response.json();
setAssets(data.assets);
setTotal(data.total);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch assets');
setAssets([]);
} finally {
setLoading(false);
}
}, [getToken, filters]);
useEffect(() => {
fetchAssets();
}, [fetchAssets]);
const toggleFavorite = useCallback(async (assetId: number) => {
try {
const token = await getToken();
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`${API_BASE_URL}/api/content-assets/${assetId}/favorite`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to toggle favorite');
}
const data = await response.json();
// Update local state
setAssets(prev =>
prev.map(asset =>
asset.id === assetId ? { ...asset, is_favorite: data.is_favorite } : asset
)
);
return data.is_favorite;
} catch (err) {
console.error('Error toggling favorite:', err);
throw err;
}
}, [getToken]);
const deleteAsset = useCallback(async (assetId: number) => {
try {
const token = await getToken();
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`${API_BASE_URL}/api/content-assets/${assetId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to delete asset');
}
// Remove from local state
setAssets(prev => prev.filter(asset => asset.id !== assetId));
setTotal(prev => prev - 1);
return true;
} catch (err) {
console.error('Error deleting asset:', err);
throw err;
}
}, [getToken]);
const trackUsage = useCallback(async (assetId: number, action: 'download' | 'share' | 'access') => {
try {
const token = await getToken();
if (!token) {
return;
}
await fetch(`${API_BASE_URL}/api/content-assets/${assetId}/usage?action=${action}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
} catch (err) {
console.error('Error tracking usage:', err);
}
}, [getToken]);
const updateAsset = useCallback(async (
assetId: number,
updates: { title?: string; description?: string; tags?: string[] }
) => {
try {
const token = await getToken();
if (!token) {
throw new Error('Not authenticated');
}
const body: any = {};
if (updates.title !== undefined) body.title = updates.title;
if (updates.description !== undefined) body.description = updates.description;
if (updates.tags !== undefined) body.tags = updates.tags; // Send as array, not comma-separated
const response = await fetch(`${API_BASE_URL}/api/content-assets/${assetId}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error('Failed to update asset');
}
const updatedAsset = await response.json();
// Update local state
setAssets(prev =>
prev.map(asset =>
asset.id === assetId ? { ...asset, ...updatedAsset } : asset
)
);
return updatedAsset;
} catch (err) {
console.error('Error updating asset:', err);
throw err;
}
}, [getToken]);
return {
assets,
loading,
error,
total,
refetch: fetchAssets,
toggleFavorite,
deleteAsset,
updateAsset,
trackUsage,
};
};

View File

@@ -156,6 +156,81 @@ export interface UpscaleResult {
metadata: Record<string, any>;
}
export interface ControlOperationMeta {
label: string;
description: string;
provider: string;
fields?: {
control_image?: boolean;
style_image?: boolean;
control_strength?: boolean;
fidelity?: boolean;
style_strength?: boolean;
aspect_ratio?: boolean;
};
}
export interface ControlImageRequestPayload {
control_image_base64: string;
operation: 'sketch' | 'structure' | 'style' | 'style_transfer';
prompt: string;
style_image_base64?: string;
negative_prompt?: string;
control_strength?: number;
fidelity?: number;
style_strength?: number;
composition_fidelity?: number;
change_strength?: number;
aspect_ratio?: string;
style_preset?: string;
seed?: number;
output_format?: string;
}
export interface ControlResult {
success: boolean;
operation: string;
provider: string;
image_base64: string;
width: number;
height: number;
metadata: Record<string, any>;
}
export interface SocialOptimizeResult {
success: boolean;
results: Array<{
platform: string;
format: string;
width: number;
height: number;
ratio: string;
image_base64: string;
safe_zone: {
top: number;
bottom: number;
left: number;
right: number;
};
}>;
total_optimized: number;
}
export interface PlatformFormat {
name: string;
width: number;
height: number;
ratio: string;
safe_zone: {
top: number;
bottom: number;
left: number;
right: number;
};
file_type: string;
max_size_mb: number;
}
export const useImageStudio = () => {
const [templates, setTemplates] = useState<Template[]>([]);
const [providers, setProviders] = useState<Record<string, Provider> | null>(null);
@@ -172,6 +247,14 @@ export const useImageStudio = () => {
const [upscaleResult, setUpscaleResult] = useState<UpscaleResult | null>(null);
const [isUpscaling, setIsUpscaling] = useState(false);
const [upscaleError, setUpscaleError] = useState<string | null>(null);
const [controlOperations, setControlOperations] = useState<Record<string, ControlOperationMeta>>({});
const [isLoadingControlOps, setIsLoadingControlOps] = useState(false);
const [isProcessingControl, setIsProcessingControl] = useState(false);
const [controlResult, setControlResult] = useState<ControlResult | null>(null);
const [controlError, setControlError] = useState<string | null>(null);
const [isOptimizing, setIsOptimizing] = useState(false);
const [optimizeResult, setOptimizeResult] = useState<SocialOptimizeResult | null>(null);
const [optimizeError, setOptimizeError] = useState<string | null>(null);
// Load templates
const loadTemplates = useCallback(async (platform?: string, category?: string) => {
@@ -351,6 +434,83 @@ export const useImageStudio = () => {
setUpscaleError(null);
}, []);
// Load control operations
const loadControlOperations = useCallback(async () => {
setIsLoadingControlOps(true);
try {
const response = await aiApiClient.get('/api/image-studio/control/operations');
setControlOperations(response.data.operations || {});
} catch (err: any) {
console.error('Failed to load control operations:', err);
} finally {
setIsLoadingControlOps(false);
}
}, []);
// Process control
const processControl = useCallback(async (payload: ControlImageRequestPayload) => {
setIsProcessingControl(true);
setControlError(null);
try {
const response = await aiApiClient.post('/api/image-studio/control/process', payload);
setControlResult(response.data);
return response.data as ControlResult;
} catch (err: any) {
console.error('Failed to process control:', err);
const message = err.response?.data?.detail || 'Failed to process control';
setControlError(message);
throw new Error(message);
} finally {
setIsProcessingControl(false);
}
}, []);
const clearControlResult = useCallback(() => {
setControlResult(null);
setControlError(null);
}, []);
// Social Optimizer
const optimizeForSocial = useCallback(async (payload: {
image_base64: string;
platforms: string[];
format_names?: Record<string, string>;
show_safe_zones?: boolean;
crop_mode?: string;
focal_point?: { x: number; y: number };
output_format?: string;
}) => {
setIsOptimizing(true);
setOptimizeError(null);
try {
const response = await aiApiClient.post('/api/image-studio/social/optimize', payload);
setOptimizeResult(response.data);
return response.data as SocialOptimizeResult;
} catch (err: any) {
console.error('Failed to optimize for social:', err);
const message = err.response?.data?.detail || 'Failed to optimize for social platforms';
setOptimizeError(message);
throw new Error(message);
} finally {
setIsOptimizing(false);
}
}, []);
const getPlatformFormats = useCallback(async (platform: string): Promise<PlatformFormat[]> => {
try {
const response = await aiApiClient.get(`/api/image-studio/social/platforms/${platform}/formats`);
return response.data.formats || [];
} catch (err: any) {
console.error(`Failed to load formats for ${platform}:`, err);
return [];
}
}, []);
const clearOptimizeResult = useCallback(() => {
setOptimizeResult(null);
setOptimizeError(null);
}, []);
return {
// State
templates,
@@ -368,6 +528,11 @@ export const useImageStudio = () => {
upscaleResult,
isUpscaling,
upscaleError,
controlOperations,
isLoadingControlOps,
isProcessingControl,
controlResult,
controlError,
// Actions
loadTemplates,
@@ -383,6 +548,15 @@ export const useImageStudio = () => {
clearEditResult,
processUpscale,
clearUpscaleResult,
loadControlOperations,
processControl,
clearControlResult,
optimizeForSocial,
getPlatformFormats,
isOptimizing,
optimizeResult,
optimizeError,
clearOptimizeResult,
};
};