Files
igny8/frontend/src/services/api.ts
2025-11-10 22:42:08 +05:00

1880 lines
58 KiB
TypeScript

// Centralized API configuration and functions
// Auto-detect API URL based on current origin (supports both IP and subdomain access)
import { useAuthStore } from '../store/authStore';
import { useAIRequestLogsStore } from '../store/aiRequestLogsStore';
function getApiBaseUrl(): string {
// First check environment variables
const envUrl = import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_API_URL;
if (envUrl) {
// Ensure env URL ends with /api
return envUrl.endsWith('/api') ? envUrl : `${envUrl}/api`;
}
// Auto-detect based on current origin
const origin = window.location.origin;
// If accessing via localhost or IP, use same origin with backend port
if (origin.includes('localhost') || origin.includes('127.0.0.1') || /^\d+\.\d+\.\d+\.\d+/.test(origin)) {
// Backend typically runs on port 8011 (external) or 8010 (internal)
// If frontend is on port 3000, backend is on 8011
if (origin.includes(':3000')) {
return origin.replace(':3000', ':8011') + '/api';
}
// If frontend is on port 7921, backend is on 7911
if (origin.includes(':7921')) {
return origin.replace(':7921', ':7911') + '/api';
}
// Default: try port 8011
return origin.split(':')[0] + ':8011/api';
}
// Production: use subdomain
return 'https://api.igny8.com/api';
}
export const API_BASE_URL = getApiBaseUrl();
// Helper function to get active site ID from store
// Uses browser-compatible approach to avoid circular dependencies
function getActiveSiteId(): number | null {
try {
// Access localStorage directly to get persisted site ID
// This avoids circular dependency issues with importing the store
const siteStorage = localStorage.getItem('site-storage');
if (siteStorage) {
const parsed = JSON.parse(siteStorage);
const activeSite = parsed?.state?.activeSite;
if (activeSite && activeSite.id) {
return activeSite.id;
}
}
return null;
} catch (error) {
// If parsing fails or store not available, return null
console.warn('Failed to get active site ID from storage:', error);
return null;
}
}
// Helper function to get active sector ID from store
// Uses browser-compatible approach to avoid circular dependencies
function getActiveSectorId(): number | null {
try {
// Access localStorage directly to get persisted sector ID
// This avoids circular dependency issues with importing the store
const sectorStorage = localStorage.getItem('sector-storage');
if (sectorStorage) {
const parsed = JSON.parse(sectorStorage);
const activeSector = parsed?.state?.activeSector;
if (activeSector && activeSector.id) {
return activeSector.id;
}
}
return null;
} catch (error) {
// If parsing fails or store not available, return null
console.warn('Failed to get active sector ID from storage:', error);
return null;
}
}
// Get auth token from store
const getAuthToken = (): string | null => {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
return parsed?.state?.token || null;
}
} catch (e) {
// Ignore parsing errors
}
return null;
};
// Get refresh token from store
const getRefreshToken = (): string | null => {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
return parsed?.state?.refreshToken || null;
}
} catch (e) {
// Ignore parsing errors
}
return null;
};
// Generic API fetch function with timeout
export async function fetchAPI(endpoint: string, options?: RequestInit & { timeout?: number }) {
const timeout = options?.timeout || 30000; // Default 30 second timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const token = getAuthToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options?.headers,
};
// Add JWT token if available
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
headers,
credentials: 'include',
signal: controller.signal,
...options,
});
clearTimeout(timeoutId);
// Check content type and length before reading body
const contentType = response.headers.get('content-type') || '';
const contentLength = response.headers.get('content-length');
// Read response body once (can only be consumed once)
const text = await response.text();
// Handle 401 Unauthorized - try to refresh token
if (response.status === 401) {
const refreshToken = getRefreshToken();
if (refreshToken) {
try {
// Try to refresh the token
const refreshResponse = await fetch(`${API_BASE_URL}/v1/auth/refresh/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh: refreshToken }),
credentials: 'include',
});
if (refreshResponse.ok) {
const refreshData = await refreshResponse.json();
if (refreshData.success && refreshData.access) {
// Update token in store
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
parsed.state.token = refreshData.access;
localStorage.setItem('auth-storage', JSON.stringify(parsed));
}
} catch (e) {
// Ignore storage errors
}
// Retry original request with new token
const newHeaders = {
...headers,
'Authorization': `Bearer ${refreshData.access}`,
};
const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: newHeaders,
credentials: 'include',
...options,
});
// Process retry response
const retryText = await retryResponse.text();
if (retryResponse.ok) {
if (retryText && retryText.trim()) {
try {
return JSON.parse(retryText);
} catch {
return retryText;
}
}
return null;
}
}
}
} catch (refreshError) {
// Refresh failed, continue with original error handling
}
}
}
// Parse error response - extract meaningful error information
if (!response.ok) {
let errorMessage = response.statusText;
let errorType = 'HTTP_ERROR';
let errorData = null;
try {
if (contentType.includes('application/json')) {
try {
errorData = JSON.parse(text);
errorMessage = errorData.error || errorData.message || errorData.detail || errorMessage;
// If the response has a success field set to false, return it as-is
// This allows callers to handle structured error responses
if (errorData.success === false && errorData.error) {
// Return the error response object instead of throwing
// This is a special case for structured error responses
return errorData;
}
// Classify error type
if (errorData.error?.includes('OperationalError')) errorType = 'DATABASE_ERROR';
else if (errorData.error?.includes('ValidationError')) errorType = 'VALIDATION_ERROR';
else if (errorData.error?.includes('PermissionDenied')) errorType = 'PERMISSION_ERROR';
else if (errorData.error?.includes('NotFound')) errorType = 'NOT_FOUND_ERROR';
else if (errorData.error?.includes('IntegrityError')) errorType = 'DATABASE_ERROR';
else if (errorData.error?.includes('RelatedObjectDoesNotExist')) errorType = 'RELATED_OBJECT_ERROR';
} catch (e) {
// JSON parse failed, use text
errorMessage = text.substring(0, 200);
}
} else {
// HTML or text response (Django debug page)
if (text.includes('<!DOCTYPE html>')) {
// Extract error title from HTML
const titleMatch = text.match(/<title>([^<]+) at ([^<]+)<\/title>/);
if (titleMatch) {
errorType = titleMatch[1].trim(); // e.g., "OperationalError"
errorMessage = `${errorType} at ${titleMatch[2].trim()}`;
} else {
// Fallback: try to extract from h1
const h1Match = text.match(/<h1[^>]*>([^<]+)<\/h1>/);
if (h1Match) {
errorMessage = h1Match[1].trim();
errorType = errorMessage.split(' ')[0]; // First word is usually error type
} else {
errorMessage = `HTTP ${response.status} Error`;
}
}
} else {
// Plain text error
errorMessage = text.substring(0, 200); // Limit length
}
}
} catch (e) {
// If parsing fails, use status text
errorMessage = response.statusText;
}
// Log structured error (not full HTML)
console.error('API Error:', {
status: response.status,
type: errorType,
message: errorMessage,
endpoint,
errorData, // Include full error data for debugging
});
// Attach error data to error object so it can be accessed in catch block
const apiError = new Error(`API Error (${response.status}): ${errorType} - ${errorMessage}`);
(apiError as any).response = errorData;
(apiError as any).status = response.status;
throw apiError;
}
// Check if response has content before parsing JSON
// DELETE requests often return 204 No Content with empty body
if (
response.status === 204 || // No Content
contentLength === '0' ||
!text || text.trim() === '' ||
(contentType && !contentType.includes('application/json'))
) {
// Return void for empty responses
return;
}
// Parse JSON response
try {
return JSON.parse(text);
} catch (e) {
// If JSON parsing fails, return text
return text;
}
} catch (error: any) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeout}ms: ${API_BASE_URL}${endpoint}`);
}
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
throw new Error(`Network Error: Unable to reach API at ${API_BASE_URL}${endpoint}. Check CORS and network connectivity.`);
}
throw error;
}
}
// Keywords-specific API functions
export interface KeywordFilters {
search?: string;
status?: string;
cluster_id?: string;
intent?: string;
difficulty_min?: number;
difficulty_max?: number;
volume_min?: number;
volume_max?: number;
page?: number;
page_size?: number;
ordering?: string;
site_id?: number; // Site filter - automatically added from siteStore
sector_id?: number; // Sector filter - for second-level filtering
}
export interface KeywordsResponse {
count: number;
next: string | null;
previous: string | null;
results: Keyword[];
}
export interface Keyword {
id: number;
seed_keyword_id: number;
seed_keyword?: SeedKeyword; // Populated by serializer
keyword: string; // Read-only property from seed_keyword
volume: number; // Read-only property from seed_keyword or volume_override
difficulty: number; // Read-only property from seed_keyword or difficulty_override
intent: string; // Read-only property from seed_keyword
volume_override?: number | null;
difficulty_override?: number | null;
cluster_id: number | null;
cluster_name?: string | null; // Optional: populated by serializer or frontend
sector_name?: string | null; // Optional: populated by serializer
status: string;
created_at: string;
updated_at: string;
}
export interface KeywordCreateData {
seed_keyword_id: number;
volume_override?: number | null;
difficulty_override?: number | null;
cluster_id?: number | null;
status: string;
}
export interface KeywordUpdateData extends Partial<KeywordCreateData> {}
export async function fetchKeywords(filters: KeywordFilters = {}): Promise<KeywordsResponse> {
const params = new URLSearchParams();
// Automatically add active site filter if not explicitly provided
// Always add site_id if there's an active site (even for admin/developer)
// The backend will respect it appropriately - admin/developer can still see all sites
// but if a specific site is selected, filter by it
if (!filters.site_id) {
const activeSiteId = getActiveSiteId();
if (activeSiteId) {
filters.site_id = activeSiteId;
}
}
// Automatically add active sector filter if not explicitly provided
// Only add if activeSector is not null (null means "All Sectors")
// ADMIN/DEV OVERRIDE: Only inject if user is not admin/developer (handled by backend)
if (filters.sector_id === undefined) {
const activeSectorId = getActiveSectorId();
// Only add sector_id if it's not null (null means "All Sectors")
if (activeSectorId !== null && activeSectorId !== undefined) {
filters.sector_id = activeSectorId;
}
}
if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status);
if (filters.cluster_id) params.append('cluster_id', filters.cluster_id);
if (filters.intent) params.append('intent', filters.intent);
if (filters.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString());
if (filters.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString());
if (filters.volume_min !== undefined) params.append('volume_min', filters.volume_min.toString());
if (filters.volume_max !== undefined) params.append('volume_max', filters.volume_max.toString());
if (filters.site_id) params.append('site_id', filters.site_id.toString());
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
if (filters.page) params.append('page', filters.page.toString());
if (filters.page_size !== undefined && filters.page_size !== null) {
params.append('page_size', filters.page_size.toString());
}
if (filters.ordering) params.append('ordering', filters.ordering);
const queryString = params.toString();
const endpoint = `/v1/planner/keywords/${queryString ? `?${queryString}` : ''}`;
return fetchAPI(endpoint);
}
export async function fetchKeyword(id: number): Promise<Keyword> {
return fetchAPI(`/v1/planner/keywords/${id}/`);
}
export async function createKeyword(data: KeywordCreateData): Promise<Keyword> {
return fetchAPI('/v1/planner/keywords/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateKeyword(id: number, data: KeywordUpdateData): Promise<Keyword> {
return fetchAPI(`/v1/planner/keywords/${id}/`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteKeyword(id: number): Promise<void> {
return fetchAPI(`/v1/planner/keywords/${id}/`, {
method: 'DELETE',
});
}
export async function bulkDeleteKeywords(ids: number[]): Promise<{ deleted_count: number }> {
return fetchAPI(`/v1/planner/keywords/bulk_delete/`, {
method: 'POST',
body: JSON.stringify({ ids }),
});
}
export async function bulkUpdateKeywordsStatus(ids: number[], status: string): Promise<{ updated_count: number }> {
return fetchAPI(`/v1/planner/keywords/bulk_update/`, {
method: 'POST',
body: JSON.stringify({ ids, status }),
});
}
// Clusters-specific API functions
export interface ClusterFilters {
search?: string;
status?: string;
difficulty_min?: number;
difficulty_max?: number;
volume_min?: number;
volume_max?: number;
page?: number;
page_size?: number;
ordering?: string;
site_id?: number; // Site filter - automatically added from siteStore
sector_id?: number; // Sector filter - for second-level filtering
}
export interface ClustersResponse {
count: number;
next: string | null;
previous: string | null;
results: Cluster[];
}
export interface Cluster {
id: number;
name: string;
description?: string | null;
keywords_count: number;
volume: number;
difficulty: number; // Average difficulty of keywords in cluster
mapped_pages: number;
status: string;
sector_name?: string | null; // Optional: populated by serializer
created_at: string;
}
export interface ClusterCreateData {
name: string;
description?: string | null;
status?: string;
}
export interface ClusterUpdateData extends Partial<ClusterCreateData> {}
export async function fetchClusters(filters: ClusterFilters = {}): Promise<ClustersResponse> {
const params = new URLSearchParams();
// Automatically add active site filter if not explicitly provided
// Always add site_id if there's an active site (even for admin/developer)
// The backend will respect it appropriately - admin/developer can still see all sites
// but if a specific site is selected, filter by it
if (!filters.site_id) {
const activeSiteId = getActiveSiteId();
if (activeSiteId) {
filters.site_id = activeSiteId;
}
}
// Automatically add active sector filter if not explicitly provided
// Only add if activeSector is not null (null means "All Sectors")
// ADMIN/DEV OVERRIDE: Only inject if user is not admin/developer (handled by backend)
if (filters.sector_id === undefined) {
const activeSectorId = getActiveSectorId();
// Only add sector_id if it's not null (null means "All Sectors")
if (activeSectorId !== null && activeSectorId !== undefined) {
filters.sector_id = activeSectorId;
}
}
if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status);
if (filters.difficulty_min !== undefined) params.append('difficulty_min', filters.difficulty_min.toString());
if (filters.difficulty_max !== undefined) params.append('difficulty_max', filters.difficulty_max.toString());
if (filters.volume_min !== undefined) params.append('volume_min', filters.volume_min.toString());
if (filters.volume_max !== undefined) params.append('volume_max', filters.volume_max.toString());
if (filters.site_id) params.append('site_id', filters.site_id.toString());
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
if (filters.page) params.append('page', filters.page.toString());
if (filters.page_size !== undefined && filters.page_size !== null) {
params.append('page_size', filters.page_size.toString());
}
if (filters.ordering) params.append('ordering', filters.ordering);
const queryString = params.toString();
const endpoint = `/v1/planner/clusters/${queryString ? `?${queryString}` : ''}`;
return fetchAPI(endpoint);
}
export async function fetchCluster(id: number): Promise<Cluster> {
return fetchAPI(`/v1/planner/clusters/${id}/`);
}
export async function createCluster(data: ClusterCreateData): Promise<Cluster> {
return fetchAPI('/v1/planner/clusters/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateCluster(id: number, data: ClusterUpdateData): Promise<Cluster> {
return fetchAPI(`/v1/planner/clusters/${id}/`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteCluster(id: number): Promise<void> {
return fetchAPI(`/v1/planner/clusters/${id}/`, {
method: 'DELETE',
});
}
export async function bulkDeleteClusters(ids: number[]): Promise<{ deleted_count: number }> {
return fetchAPI(`/v1/planner/clusters/bulk_delete/`, {
method: 'POST',
body: JSON.stringify({ ids }),
});
}
export async function bulkUpdateClustersStatus(ids: number[], status: string): Promise<{ updated_count: number }> {
return fetchAPI(`/v1/planner/clusters/bulk_update/`, {
method: 'POST',
body: JSON.stringify({ ids, status }),
});
}
export async function autoClusterKeywords(keywordIds: number[], sectorId?: number): Promise<{ success: boolean; task_id?: string; clusters_created?: number; keywords_updated?: number; message?: string; error?: string }> {
const startTime = Date.now();
const addLog = useAIRequestLogsStore.getState().addLog;
const endpoint = `/v1/planner/keywords/auto_cluster/`;
const requestBody = { ids: keywordIds, sector_id: sectorId };
const pendingLogId = addLog({
function: 'autoClusterKeywords',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
status: 'pending',
});
try {
const response = await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const duration = Date.now() - startTime;
const updateLog = useAIRequestLogsStore.getState().updateLog;
// Update log with response data (including task_id for progress tracking)
if (pendingLogId && response) {
updateLog(pendingLogId, {
response: {
status: 200,
data: response,
},
status: response.success === false ? 'error' : 'success',
duration,
});
}
// Check if response indicates an error (success: false)
if (response && response.success === false) {
// Return error response as-is so caller can check result.success
return response;
}
return response;
} catch (error: any) {
const duration = Date.now() - startTime;
// Try to extract error response data if available
let errorResponseData = null;
let errorRequestSteps = null;
// Check if error has response data (from fetchAPI)
if (error.response || error.data) {
errorResponseData = error.response || error.data;
errorRequestSteps = errorResponseData?.request_steps;
} else if ((error as any).response) {
// Error object from fetchAPI has response attached
errorResponseData = (error as any).response;
errorRequestSteps = errorResponseData?.request_steps;
}
// Parse error message to extract error type
let errorType = 'UNKNOWN_ERROR';
let errorMessage = error.message || 'Unknown error';
// Check if error response contains JSON with error field
if (error.message && error.message.includes('API Error')) {
// Try to extract structured error from API response
const apiErrorMatch = error.message.match(/API Error \(\d+\): ([^-]+) - (.+)/);
if (apiErrorMatch) {
errorType = apiErrorMatch[1].trim();
errorMessage = apiErrorMatch[2].trim();
}
}
if (errorMessage.includes('OperationalError')) {
errorType = 'DATABASE_ERROR';
errorMessage = errorMessage.replace(/API Error \(\d+\): /, '').replace(/ - .*OperationalError.*/, ' - Database operation failed');
} else if (errorMessage.includes('ValidationError')) {
errorType = 'VALIDATION_ERROR';
} else if (errorMessage.includes('PermissionDenied')) {
errorType = 'PERMISSION_ERROR';
} else if (errorMessage.includes('NotFound')) {
errorType = 'NOT_FOUND_ERROR';
} else if (errorMessage.includes('IntegrityError')) {
errorType = 'DATABASE_ERROR';
} else if (errorMessage.includes('RelatedObjectDoesNotExist')) {
errorType = 'RELATED_OBJECT_ERROR';
// Extract clean error message
errorMessage = errorMessage.replace(/API Error \(\d+\): [^-]+ - /, '').trim();
}
// Update existing log or create new one
const updateLog = useAIRequestLogsStore.getState().updateLog;
const addRequestStep = useAIRequestLogsStore.getState().addRequestStep;
if (pendingLogId) {
updateLog(pendingLogId, {
response: {
status: errorResponseData?.status || 500,
error: errorMessage,
errorType,
data: errorResponseData,
},
status: 'error',
duration,
});
// Add request steps from error response if available
if (errorRequestSteps && Array.isArray(errorRequestSteps)) {
errorRequestSteps.forEach((step: any) => {
addRequestStep(pendingLogId, step);
});
}
} else {
// Create new log if pendingLogId doesn't exist
const errorLogId = addLog({
function: 'autoClusterKeywords',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: errorResponseData?.status || 500,
error: errorMessage,
errorType,
data: errorResponseData,
},
status: 'error',
duration,
});
if (errorLogId && errorRequestSteps && Array.isArray(errorRequestSteps)) {
errorRequestSteps.forEach((step: any) => {
addRequestStep(errorLogId, step);
});
}
}
// Return error response in same format as successful response
// This allows the caller to check result.success === false
return {
success: false,
error: errorMessage,
errorType,
};
}
}
export async function autoGenerateIdeas(clusterIds: number[]): Promise<{ success: boolean; task_id?: string; ideas_created?: number; message?: string; error?: string }> {
const startTime = Date.now();
const { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null }));
const addLog = useAIRequestLogsStore?.getState().addLog;
const endpoint = `/v1/planner/clusters/auto_generate_ideas/`;
const requestBody = { ids: clusterIds };
addLog?.({
function: 'autoGenerateIdeas',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
status: 'pending',
});
try {
const response = await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const duration = Date.now() - startTime;
addLog({
function: 'autoGenerateIdeas',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 200,
data: response,
},
status: 'success',
duration,
});
return response;
} catch (error: any) {
const duration = Date.now() - startTime;
// Parse error message to extract error type
let errorType = 'UNKNOWN_ERROR';
let errorMessage = error.message || 'Unknown error';
if (errorMessage.includes('OperationalError')) {
errorType = 'DATABASE_ERROR';
errorMessage = errorMessage.replace(/API Error \(\d+\): /, '').replace(/ - .*OperationalError.*/, ' - Database operation failed');
} else if (errorMessage.includes('ValidationError')) {
errorType = 'VALIDATION_ERROR';
} else if (errorMessage.includes('PermissionDenied')) {
errorType = 'PERMISSION_ERROR';
} else if (errorMessage.match(/API Error \(\d+\): ([^-]+)/)) {
const match = errorMessage.match(/API Error \(\d+\): ([^-]+)/);
if (match) {
errorType = match[1].trim();
errorMessage = errorMessage.replace(/API Error \(\d+\): [^-]+ - /, '');
}
}
addLog?.({
function: 'autoGenerateIdeas',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 500,
error: errorMessage,
errorType,
},
status: 'error',
duration,
});
throw error;
}
}
export async function generateSingleIdea(ideaId: string | number, clusterId: number): Promise<{ success: boolean; task_id?: string; idea_created?: number; message?: string; error?: string }> {
const startTime = Date.now();
const { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null }));
const addLog = useAIRequestLogsStore?.getState().addLog;
const endpoint = `/v1/planner/ideas/${ideaId}/generate_idea/`;
const requestBody = { cluster_id: clusterId };
addLog?.({
function: 'generateSingleIdea',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
status: 'pending',
});
try {
const response = await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const duration = Date.now() - startTime;
addLog?.({
function: 'generateSingleIdea',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 200,
data: response,
},
status: 'success',
duration,
});
return response;
} catch (error: any) {
const duration = Date.now() - startTime;
// Parse error message to extract error type
let errorType = 'UNKNOWN_ERROR';
let errorMessage = error.message || 'Unknown error';
if (errorMessage.includes('OperationalError')) {
errorType = 'DATABASE_ERROR';
errorMessage = errorMessage.replace(/API Error \(\d+\): /, '').replace(/ - .*OperationalError.*/, ' - Database operation failed');
} else if (errorMessage.includes('ValidationError')) {
errorType = 'VALIDATION_ERROR';
} else if (errorMessage.includes('PermissionDenied')) {
errorType = 'PERMISSION_ERROR';
} else if (errorMessage.match(/API Error \(\d+\): ([^-]+)/)) {
const match = errorMessage.match(/API Error \(\d+\): ([^-]+)/);
if (match) {
errorType = match[1].trim();
errorMessage = errorMessage.replace(/API Error \(\d+\): [^-]+ - /, '');
}
}
addLog?.({
function: 'generateSingleIdea',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 500,
error: errorMessage,
errorType,
},
status: 'error',
duration,
});
throw error;
}
}
// Helper function to fetch all clusters (for dropdowns)
export async function fetchAllClusters(): Promise<Cluster[]> {
const response = await fetchClusters({ ordering: 'name' });
return response.results;
}
// ContentIdeas API functions
export interface ContentIdeasFilters {
search?: string;
status?: string;
keyword_cluster_id?: string;
content_structure?: string;
content_type?: string;
page?: number;
page_size?: number;
ordering?: string;
site_id?: number; // Site filter - automatically added from siteStore
sector_id?: number; // Sector filter - for second-level filtering
}
export interface ContentIdeasResponse {
count: number;
next: string | null;
previous: string | null;
results: ContentIdea[];
}
export interface ContentIdea {
id: number;
idea_title: string;
description?: string | null;
content_structure: string;
content_type: string;
target_keywords?: string | null;
keyword_cluster_id?: number | null;
keyword_cluster_name?: string | null;
sector_name?: string | null; // Optional: populated by serializer
status: string;
estimated_word_count: number;
created_at: string;
updated_at: string;
}
export interface ContentIdeaCreateData {
idea_title: string;
description?: string | null;
content_structure?: string;
content_type?: string;
target_keywords?: string | null;
keyword_cluster_id?: number | null;
status?: string;
estimated_word_count?: number;
}
export interface ContentIdeaUpdateData extends Partial<ContentIdeaCreateData> {}
export async function fetchContentIdeas(filters: ContentIdeasFilters = {}): Promise<ContentIdeasResponse> {
const params = new URLSearchParams();
// Automatically add active site filter if not explicitly provided
// Always add site_id if there's an active site (even for admin/developer)
// The backend will respect it appropriately - admin/developer can still see all sites
// but if a specific site is selected, filter by it
if (!filters.site_id) {
const activeSiteId = getActiveSiteId();
if (activeSiteId) {
filters.site_id = activeSiteId;
}
}
// Automatically add active sector filter if not explicitly provided
// Only add if activeSector is not null (null means "All Sectors")
// ADMIN/DEV OVERRIDE: Only inject if user is not admin/developer (handled by backend)
if (filters.sector_id === undefined) {
const activeSectorId = getActiveSectorId();
// Only add sector_id if it's not null (null means "All Sectors")
if (activeSectorId !== null && activeSectorId !== undefined) {
filters.sector_id = activeSectorId;
}
}
if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status);
if (filters.keyword_cluster_id) params.append('keyword_cluster_id', filters.keyword_cluster_id);
if (filters.content_structure) params.append('content_structure', filters.content_structure);
if (filters.content_type) params.append('content_type', filters.content_type);
if (filters.site_id) params.append('site_id', filters.site_id.toString());
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
if (filters.page) params.append('page', filters.page.toString());
if (filters.page_size !== undefined && filters.page_size !== null) {
params.append('page_size', filters.page_size.toString());
}
if (filters.ordering) params.append('ordering', filters.ordering);
const queryString = params.toString();
const endpoint = `/v1/planner/ideas/${queryString ? `?${queryString}` : ''}`;
return fetchAPI(endpoint);
}
export async function fetchContentIdea(id: number): Promise<ContentIdea> {
return fetchAPI(`/v1/planner/ideas/${id}/`);
}
export async function createContentIdea(data: ContentIdeaCreateData): Promise<ContentIdea> {
return fetchAPI('/v1/planner/ideas/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateContentIdea(id: number, data: ContentIdeaUpdateData): Promise<ContentIdea> {
return fetchAPI(`/v1/planner/ideas/${id}/`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteContentIdea(id: number): Promise<void> {
return fetchAPI(`/v1/planner/ideas/${id}/`, {
method: 'DELETE',
});
}
export async function bulkDeleteContentIdeas(ids: number[]): Promise<{ deleted_count: number }> {
return fetchAPI(`/v1/planner/ideas/bulk_delete/`, {
method: 'POST',
body: JSON.stringify({ ids }),
});
}
export async function bulkUpdateContentIdeasStatus(ids: number[], status: string): Promise<{ updated_count: number }> {
return fetchAPI(`/v1/planner/ideas/bulk_update/`, {
method: 'POST',
body: JSON.stringify({ ids, status }),
});
}
export async function bulkQueueIdeasToWriter(ids: number[]): Promise<{ created_count: number; task_ids: number[] }> {
return fetchAPI(`/v1/planner/ideas/bulk_queue_to_writer/`, {
method: 'POST',
body: JSON.stringify({ ids }),
});
}
// Tasks API functions
export interface TasksFilters {
search?: string;
status?: string;
cluster_id?: string;
content_type?: string;
content_structure?: string;
page?: number;
page_size?: number;
ordering?: string;
site_id?: number; // Site filter - automatically added from siteStore
sector_id?: number; // Sector filter - for second-level filtering
}
export interface TasksResponse {
count: number;
next: string | null;
previous: string | null;
results: Task[];
}
export interface Task {
id: number;
title: string;
description?: string | null;
keywords?: string | null;
cluster_id?: number | null;
cluster_name?: string | null;
sector_name?: string | null; // Optional: populated by serializer
idea_id?: number | null;
idea_title?: string | null;
content_structure: string;
content_type: string;
status: string;
content?: string | null;
word_count: number;
meta_title?: string | null;
meta_description?: string | null;
content_html?: string | null;
content_primary_keyword?: string | null;
content_secondary_keywords?: string[];
content_tags?: string[];
content_categories?: string[];
assigned_post_id?: number | null;
post_url?: string | null;
created_at: string;
updated_at: string;
}
export interface TaskCreateData {
title: string;
description?: string | null;
keywords?: string | null;
cluster_id?: number | null;
idea_id?: number | null;
content_structure?: string;
content_type?: string;
status?: string;
content?: string | null;
word_count?: number;
meta_title?: string | null;
meta_description?: string | null;
}
export interface TaskUpdateData extends Partial<TaskCreateData> {}
export async function fetchTasks(filters: TasksFilters = {}): Promise<TasksResponse> {
const params = new URLSearchParams();
// Automatically add active site filter if not explicitly provided
// Always add site_id if there's an active site (even for admin/developer)
// The backend will respect it appropriately - admin/developer can still see all sites
// but if a specific site is selected, filter by it
if (!filters.site_id) {
const activeSiteId = getActiveSiteId();
if (activeSiteId) {
filters.site_id = activeSiteId;
}
}
// Automatically add active sector filter if not explicitly provided
// Only add if activeSector is not null (null means "All Sectors")
// ADMIN/DEV OVERRIDE: Only inject if user is not admin/developer (handled by backend)
if (filters.sector_id === undefined) {
const activeSectorId = getActiveSectorId();
// Only add sector_id if it's not null (null means "All Sectors")
if (activeSectorId !== null && activeSectorId !== undefined) {
filters.sector_id = activeSectorId;
}
}
if (filters.search) params.append('search', filters.search);
if (filters.status) params.append('status', filters.status);
if (filters.cluster_id) params.append('cluster_id', filters.cluster_id);
if (filters.content_type) params.append('content_type', filters.content_type);
if (filters.content_structure) params.append('content_structure', filters.content_structure);
if (filters.site_id) params.append('site_id', filters.site_id.toString());
if (filters.sector_id) params.append('sector_id', filters.sector_id.toString());
if (filters.page) params.append('page', filters.page.toString());
if (filters.page_size !== undefined && filters.page_size !== null) {
params.append('page_size', filters.page_size.toString());
}
if (filters.ordering) params.append('ordering', filters.ordering);
const queryString = params.toString();
const endpoint = `/v1/writer/tasks/${queryString ? `?${queryString}` : ''}`;
return fetchAPI(endpoint);
}
export async function fetchTask(id: number): Promise<Task> {
return fetchAPI(`/v1/writer/tasks/${id}/`);
}
export async function createTask(data: TaskCreateData): Promise<Task> {
return fetchAPI('/v1/writer/tasks/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateTask(id: number, data: TaskUpdateData): Promise<Task> {
return fetchAPI(`/v1/writer/tasks/${id}/`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteTask(id: number): Promise<void> {
return fetchAPI(`/v1/writer/tasks/${id}/`, {
method: 'DELETE',
});
}
export async function bulkDeleteTasks(ids: number[]): Promise<{ deleted_count: number }> {
return fetchAPI(`/v1/writer/tasks/bulk_delete/`, {
method: 'POST',
body: JSON.stringify({ ids }),
});
}
export async function bulkUpdateTasksStatus(ids: number[], status: string): Promise<{ updated_count: number }> {
return fetchAPI(`/v1/writer/tasks/bulk_update/`, {
method: 'POST',
body: JSON.stringify({ ids, status }),
});
}
export async function autoGenerateContent(ids: number[]): Promise<{ success: boolean; task_id?: string; tasks_updated?: number; message?: string; error?: string }> {
const startTime = Date.now();
const { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null }));
const addLog = useAIRequestLogsStore?.getState().addLog;
const endpoint = `/v1/writer/tasks/auto_generate_content/`;
const requestBody = { ids };
addLog?.({
function: 'autoGenerateContent',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
status: 'pending',
});
try {
const response = await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const duration = Date.now() - startTime;
addLog({
function: 'autoGenerateContent',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 200,
data: response,
},
status: 'success',
duration,
});
return response;
} catch (error: any) {
const duration = Date.now() - startTime;
// Parse error message to extract error type
let errorType = 'UNKNOWN_ERROR';
let errorMessage = error.message || 'Unknown error';
if (errorMessage.includes('OperationalError')) {
errorType = 'DATABASE_ERROR';
errorMessage = errorMessage.replace(/API Error \(\d+\): /, '').replace(/ - .*OperationalError.*/, ' - Database operation failed');
} else if (errorMessage.includes('ValidationError')) {
errorType = 'VALIDATION_ERROR';
} else if (errorMessage.match(/API Error \(\d+\): ([^-]+)/)) {
const match = errorMessage.match(/API Error \(\d+\): ([^-]+)/);
if (match) {
errorType = match[1].trim();
errorMessage = errorMessage.replace(/API Error \(\d+\): [^-]+ - /, '');
}
}
addLog?.({
function: 'autoGenerateContent',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 500,
error: errorMessage,
errorType,
},
status: 'error',
duration,
});
throw error;
}
}
export async function autoGenerateImages(taskIds: number[]): Promise<{ success: boolean; task_id?: string; images_created?: number; message?: string; error?: string }> {
const startTime = Date.now();
const { useAIRequestLogsStore } = await import('../store/aiRequestLogsStore').catch(() => ({ useAIRequestLogsStore: null }));
const addLog = useAIRequestLogsStore?.getState().addLog;
const endpoint = `/v1/writer/tasks/auto_generate_images/`;
const requestBody = { task_ids: taskIds };
addLog?.({
function: 'autoGenerateImages',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
status: 'pending',
});
try {
const response = await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const duration = Date.now() - startTime;
addLog({
function: 'autoGenerateImages',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 200,
data: response,
},
status: 'success',
duration,
});
return response;
} catch (error: any) {
const duration = Date.now() - startTime;
// Parse error message to extract error type
let errorType = 'UNKNOWN_ERROR';
let errorMessage = error.message || 'Unknown error';
if (errorMessage.includes('OperationalError')) {
errorType = 'DATABASE_ERROR';
errorMessage = errorMessage.replace(/API Error \(\d+\): /, '').replace(/ - .*OperationalError.*/, ' - Database operation failed');
} else if (errorMessage.includes('ValidationError')) {
errorType = 'VALIDATION_ERROR';
} else if (errorMessage.match(/API Error \(\d+\): ([^-]+)/)) {
const match = errorMessage.match(/API Error \(\d+\): ([^-]+)/);
if (match) {
errorType = match[1].trim();
errorMessage = errorMessage.replace(/API Error \(\d+\): [^-]+ - /, '');
}
}
addLog?.({
function: 'autoGenerateImages',
endpoint,
request: {
method: 'POST',
body: requestBody,
},
response: {
status: 500,
error: errorMessage,
errorType,
},
status: 'error',
duration,
});
throw error;
}
}
// TaskImages API functions
export interface TaskImage {
id: number;
task_id: number;
task_title?: string | null;
image_type: string;
image_url?: string | null;
image_path?: string | null;
prompt?: string | null;
status: string;
position: number;
created_at: string;
updated_at: string;
}
export interface TaskImageFilters {
task_id?: string;
image_type?: string;
status?: string;
page?: number;
ordering?: string;
}
export interface TaskImagesResponse {
count: number;
next: string | null;
previous: string | null;
results: TaskImage[];
}
export async function fetchTaskImages(filters: TaskImageFilters = {}): Promise<TaskImagesResponse> {
const params = new URLSearchParams();
if (filters.task_id) params.append('task_id', filters.task_id);
if (filters.image_type) params.append('image_type', filters.image_type);
if (filters.status) params.append('status', filters.status);
if (filters.page) params.append('page', filters.page.toString());
if (filters.ordering) params.append('ordering', filters.ordering || 'task,position,-created_at');
const queryString = params.toString();
const endpoint = `/v1/writer/images/${queryString ? `?${queryString}` : ''}`;
return fetchAPI(endpoint);
}
export async function deleteTaskImage(id: number): Promise<void> {
return fetchAPI(`/v1/writer/images/${id}/`, {
method: 'DELETE',
});
}
// Sites API functions
export interface Site {
id: number;
name: string;
slug: string;
domain?: string | null;
description?: string | null;
industry?: number | null;
industry_name?: string | null;
industry_slug?: string | null;
is_active: boolean;
status: string;
wp_url?: string | null;
wp_username?: string | null;
sectors_count: number;
active_sectors_count: number;
selected_sectors: number[];
can_add_sectors: boolean;
created_at: string;
updated_at: string;
}
export interface SiteCreateData {
name: string;
slug?: string;
domain?: string;
description?: string;
is_active?: boolean;
status?: string;
wp_url?: string;
wp_username?: string;
wp_app_password?: string;
}
export interface SitesResponse {
count: number;
next: string | null;
previous: string | null;
results: Site[];
}
export async function fetchSites(): Promise<SitesResponse> {
return fetchAPI('/v1/auth/sites/');
}
export async function fetchSite(id: number): Promise<Site> {
return fetchAPI(`/v1/auth/sites/${id}/`);
}
export async function createSite(data: SiteCreateData): Promise<Site> {
return fetchAPI('/v1/auth/sites/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateSite(id: number, data: Partial<SiteCreateData>): Promise<Site> {
return fetchAPI(`/v1/auth/sites/${id}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
}
export async function deleteSite(id: number): Promise<void> {
return fetchAPI(`/v1/auth/sites/${id}/`, {
method: 'DELETE',
});
}
export async function setActiveSite(id: number): Promise<{ success: boolean; message: string; site: Site }> {
return fetchAPI(`/v1/auth/sites/${id}/set_active/`, {
method: 'POST',
});
}
export async function selectSectorsForSite(
siteId: number,
industrySlug: string,
sectorSlugs: string[]
): Promise<{ success: boolean; message: string; created_count: number; updated_count: number; sectors: any[] }> {
return fetchAPI(`/v1/auth/sites/${siteId}/select_sectors/`, {
method: 'POST',
body: JSON.stringify({
industry_slug: industrySlug,
sector_slugs: sectorSlugs,
}),
});
}
export async function fetchSiteSectors(siteId: number): Promise<any[]> {
return fetchAPI(`/v1/auth/sites/${siteId}/sectors/`);
}
// Industries API functions
export interface Industry {
name: string;
slug: string;
description: string;
sectors: Sector[];
}
export interface Sector {
name: string;
slug: string;
description: string;
}
export interface IndustriesResponse {
success: boolean;
industries: Industry[];
}
export async function fetchIndustries(): Promise<IndustriesResponse> {
return fetchAPI('/v1/auth/industries/');
}
// Sectors API functions
// Settings API functions
export interface AccountSetting {
id: number;
key: string;
config: Record<string, any>;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface AccountSettingsResponse {
count: number;
next: string | null;
previous: string | null;
results: AccountSetting[];
}
export async function fetchAccountSettings(): Promise<AccountSettingsResponse> {
return fetchAPI('/v1/system/settings/account/');
}
export async function fetchAccountSetting(key: string): Promise<AccountSetting> {
return fetchAPI(`/v1/system/settings/account/${key}/`);
}
export async function createAccountSetting(data: { key: string; config: Record<string, any>; is_active?: boolean }): Promise<AccountSetting> {
return fetchAPI('/v1/system/settings/account/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateAccountSetting(key: string, data: Partial<{ config: Record<string, any>; is_active: boolean }>): Promise<AccountSetting> {
return fetchAPI(`/v1/system/settings/account/${key}/`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteAccountSetting(key: string): Promise<void> {
return fetchAPI(`/v1/system/settings/account/${key}/`, {
method: 'DELETE',
});
}
// Module Settings
export interface ModuleSetting {
id: number;
module_name: string;
key: string;
config: Record<string, any>;
is_active: boolean;
created_at: string;
updated_at: string;
}
export async function fetchModuleSettings(moduleName: string): Promise<ModuleSetting[]> {
const response = await fetchAPI(`/v1/system/settings/modules/module/${moduleName}/`);
return response.results || [];
}
export async function createModuleSetting(data: { module_name: string; key: string; config: Record<string, any>; is_active?: boolean }): Promise<ModuleSetting> {
return fetchAPI('/v1/system/settings/modules/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateModuleSetting(moduleName: string, key: string, data: Partial<{ config: Record<string, any>; is_active: boolean }>): Promise<ModuleSetting> {
return fetchAPI(`/v1/system/settings/modules/${key}/?module_name=${moduleName}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
// Billing API functions
export interface CreditBalance {
credits: number;
plan_credits_per_month: number;
credits_used_this_month: number;
credits_remaining: number;
}
export interface CreditUsageLog {
id: number;
operation_type: string;
operation_type_display: string;
credits_used: number;
cost_usd: string | null;
model_used: string;
tokens_input: number | null;
tokens_output: number | null;
related_object_type: string;
related_object_id: number | null;
metadata: Record<string, any>;
created_at: string;
}
export interface CreditUsageResponse {
count: number;
next: string | null;
previous: string | null;
results: CreditUsageLog[];
}
export interface UsageSummary {
period: {
start: string;
end: string;
};
total_credits_used: number;
total_cost_usd: number;
by_operation: Record<string, {
credits: number;
cost: number;
count: number;
}>;
by_model: Record<string, {
credits: number;
cost: number;
}>;
}
export async function fetchCreditBalance(): Promise<CreditBalance> {
return fetchAPI('/v1/billing/credits/balance/balance/');
}
export async function fetchCreditUsage(filters?: {
operation_type?: string;
start_date?: string;
end_date?: string;
page?: number;
}): Promise<CreditUsageResponse> {
const params = new URLSearchParams();
if (filters?.operation_type) params.append('operation_type', filters.operation_type);
if (filters?.start_date) params.append('start_date', filters.start_date);
if (filters?.end_date) params.append('end_date', filters.end_date);
if (filters?.page) params.append('page', filters.page.toString());
const queryString = params.toString();
return fetchAPI(`/v1/billing/credits/usage/${queryString ? `?${queryString}` : ''}`);
}
export async function fetchUsageSummary(startDate?: string, endDate?: string): Promise<UsageSummary> {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
const queryString = params.toString();
return fetchAPI(`/v1/billing/credits/usage/summary/${queryString ? `?${queryString}` : ''}`);
}
export interface LimitCard {
title: string;
limit: number;
used: number;
available: number;
unit: string;
category: 'planner' | 'writer' | 'images' | 'ai' | 'general';
percentage: number;
}
export interface UsageLimitsResponse {
limits: LimitCard[];
}
export async function fetchUsageLimits(): Promise<UsageLimitsResponse> {
console.log('Fetching usage limits from:', '/v1/billing/credits/usage/limits/');
try {
const response = await fetchAPI('/v1/billing/credits/usage/limits/');
console.log('Usage limits API response:', response);
return response;
} catch (error) {
console.error('Error fetching usage limits:', error);
throw error;
}
}
export interface CreditTransaction {
id: number;
transaction_type: string;
transaction_type_display: string;
amount: number;
balance_after: number;
description: string;
metadata: Record<string, any>;
created_at: string;
}
export interface CreditTransactionResponse {
count: number;
next: string | null;
previous: string | null;
results: CreditTransaction[];
}
export async function fetchCreditTransactions(filters?: {
transaction_type?: string;
page?: number;
}): Promise<CreditTransactionResponse> {
const params = new URLSearchParams();
if (filters?.transaction_type) params.append('transaction_type', filters.transaction_type);
if (filters?.page) params.append('page', filters.page.toString());
const queryString = params.toString();
return fetchAPI(`/v1/billing/credits/transactions/${queryString ? `?${queryString}` : ''}`);
}
// Seed Keywords API
export interface SeedKeyword {
id: number;
keyword: string;
industry: number;
industry_name: string;
industry_slug: string;
sector: number;
sector_name: string;
sector_slug: string;
volume: number;
difficulty: number;
intent: string;
intent_display: string;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface SeedKeywordResponse {
count: number;
next: string | null;
previous: string | null;
results: SeedKeyword[];
}
export async function fetchSeedKeywords(filters?: {
industry?: number;
sector?: number;
intent?: string;
search?: string;
page?: number;
page_size?: number;
}): Promise<SeedKeywordResponse> {
const params = new URLSearchParams();
// Use industry_id and sector_id as per backend get_queryset, but also try industry/sector for filterset_fields
if (filters?.industry) {
params.append('industry', filters.industry.toString());
params.append('industry_id', filters.industry.toString()); // Also send industry_id for get_queryset
}
if (filters?.sector) {
params.append('sector', filters.sector.toString());
params.append('sector_id', filters.sector.toString()); // Also send sector_id for get_queryset
}
if (filters?.intent) params.append('intent', filters.intent);
if (filters?.search) params.append('search', filters.search);
if (filters?.page) params.append('page', filters.page.toString());
if (filters?.page_size) params.append('page_size', filters.page_size.toString());
const queryString = params.toString();
return fetchAPI(`/v1/auth/seed-keywords/${queryString ? `?${queryString}` : ''}`);
}
/**
* Add SeedKeywords to workflow (create Keywords records)
*/
export async function addSeedKeywordsToWorkflow(seedKeywordIds: number[], siteId: number, sectorId: number): Promise<{ success: boolean; created: number; errors?: string[] }> {
return fetchAPI('/v1/planner/keywords/bulk_add_from_seed/', {
method: 'POST',
body: JSON.stringify({
seed_keyword_ids: seedKeywordIds,
site_id: siteId,
sector_id: sectorId,
}),
});
}
// Author Profiles API
export interface AuthorProfile {
id: number;
name: string;
description: string;
tone: string;
language: string;
structure_template: Record<string, any>;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface AuthorProfileResponse {
count: number;
next: string | null;
previous: string | null;
results: AuthorProfile[];
}
export async function fetchAuthorProfiles(filters?: {
is_active?: boolean;
language?: string;
search?: string;
page?: number;
}): Promise<AuthorProfileResponse> {
const params = new URLSearchParams();
if (filters?.is_active !== undefined) params.append('is_active', filters.is_active.toString());
if (filters?.language) params.append('language', filters.language);
if (filters?.search) params.append('search', filters.search);
if (filters?.page) params.append('page', filters.page.toString());
const queryString = params.toString();
return fetchAPI(`/v1/system/author-profiles/${queryString ? `?${queryString}` : ''}`);
}
export async function createAuthorProfile(data: Partial<AuthorProfile>): Promise<AuthorProfile> {
return fetchAPI('/v1/system/author-profiles/', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateAuthorProfile(id: number, data: Partial<AuthorProfile>): Promise<AuthorProfile> {
return fetchAPI(`/v1/system/author-profiles/${id}/`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteAuthorProfile(id: number): Promise<void> {
return fetchAPI(`/v1/system/author-profiles/${id}/`, {
method: 'DELETE',
});
}
// Content API
export interface Content {
id: number;
task: number;
task_title?: string | null;
title?: string | null;
meta_title?: string | null;
meta_description?: string | null;
primary_keyword?: string | null;
secondary_keywords?: string[];
tags?: string[];
categories?: string[];
status: string;
html_content: string;
word_count: number;
metadata: Record<string, any>;
generated_at: string;
updated_at: string;
}
export interface ContentResponse {
count: number;
next: string | null;
previous: string | null;
results: Content[];
}
export async function fetchContent(filters?: {
task_id?: number;
page?: number;
}): Promise<ContentResponse> {
const params = new URLSearchParams();
if (filters?.task_id) params.append('task_id', filters.task_id.toString());
if (filters?.page) params.append('page', filters.page.toString());
const queryString = params.toString();
return fetchAPI(`/v1/writer/content/${queryString ? `?${queryString}` : ''}`);
}