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:
244
frontend/src/hooks/useContentAssets.ts
Normal file
244
frontend/src/hooks/useContentAssets.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user