Stage 3 & stage 4
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
// 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
|
||||
@@ -575,43 +574,15 @@ export async function bulkUpdateClustersStatus(ids: number[], status: string): P
|
||||
}
|
||||
|
||||
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
|
||||
@@ -620,108 +591,7 @@ export async function autoClusterKeywords(keywordIds: number[], sectorId?: numbe
|
||||
|
||||
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,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface AIStepLog {
|
||||
stepNumber: number;
|
||||
stepName: string;
|
||||
functionName: string;
|
||||
status: 'pending' | 'success' | 'error';
|
||||
timestamp: Date;
|
||||
message?: string;
|
||||
error?: string;
|
||||
duration?: number; // milliseconds
|
||||
}
|
||||
|
||||
export interface AIRequestLog {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
function: string; // e.g., 'autoClusterKeywords', 'autoGenerateIdeas', 'autoGenerateContent', 'autoGenerateImages'
|
||||
endpoint: string;
|
||||
request: {
|
||||
method: string;
|
||||
body?: any;
|
||||
params?: any;
|
||||
};
|
||||
response?: {
|
||||
status: number;
|
||||
data?: any;
|
||||
error?: string;
|
||||
errorType?: string; // e.g., 'DATABASE_ERROR', 'VALIDATION_ERROR', 'PERMISSION_ERROR'
|
||||
};
|
||||
status: 'pending' | 'success' | 'error';
|
||||
duration?: number; // milliseconds
|
||||
requestSteps: AIStepLog[]; // Request steps (INIT, PREP, SAVE, DONE)
|
||||
responseSteps: AIStepLog[]; // Response steps (AI_CALL, PARSE)
|
||||
}
|
||||
|
||||
interface AIRequestLogsStore {
|
||||
logs: AIRequestLog[];
|
||||
addLog: (log: Omit<AIRequestLog, 'id' | 'timestamp' | 'requestSteps' | 'responseSteps'>) => string;
|
||||
updateLog: (logId: string, updates: Partial<AIRequestLog>) => void;
|
||||
addRequestStep: (logId: string, step: Omit<AIStepLog, 'timestamp'>) => void;
|
||||
addResponseStep: (logId: string, step: Omit<AIStepLog, 'timestamp'>) => void;
|
||||
clearLogs: () => void;
|
||||
maxLogs: number;
|
||||
}
|
||||
|
||||
export const useAIRequestLogsStore = create<AIRequestLogsStore>((set, get) => ({
|
||||
logs: [],
|
||||
maxLogs: 20, // Keep last 20 logs
|
||||
|
||||
addLog: (log) => {
|
||||
const newLog: AIRequestLog = {
|
||||
...log,
|
||||
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: new Date(),
|
||||
requestSteps: [],
|
||||
responseSteps: [],
|
||||
};
|
||||
|
||||
set((state) => {
|
||||
const updatedLogs = [newLog, ...state.logs].slice(0, state.maxLogs);
|
||||
return { logs: updatedLogs };
|
||||
});
|
||||
|
||||
// Return the log ID so callers can add steps
|
||||
return newLog.id;
|
||||
},
|
||||
|
||||
updateLog: (logId, updates) => {
|
||||
set((state) => {
|
||||
const updatedLogs = state.logs.map((log) => {
|
||||
if (log.id === logId) {
|
||||
return { ...log, ...updates };
|
||||
}
|
||||
return log;
|
||||
});
|
||||
return { logs: updatedLogs };
|
||||
});
|
||||
},
|
||||
|
||||
addRequestStep: (logId, step) => {
|
||||
set((state) => {
|
||||
const updatedLogs = state.logs.map((log) => {
|
||||
if (log.id === logId) {
|
||||
const stepWithTimestamp: AIStepLog = {
|
||||
...step,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
return {
|
||||
...log,
|
||||
requestSteps: [...log.requestSteps, stepWithTimestamp],
|
||||
};
|
||||
}
|
||||
return log;
|
||||
});
|
||||
return { logs: updatedLogs };
|
||||
});
|
||||
},
|
||||
|
||||
addResponseStep: (logId, step) => {
|
||||
set((state) => {
|
||||
const updatedLogs = state.logs.map((log) => {
|
||||
if (log.id === logId) {
|
||||
const stepWithTimestamp: AIStepLog = {
|
||||
...step,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
return {
|
||||
...log,
|
||||
responseSteps: [...log.responseSteps, stepWithTimestamp],
|
||||
};
|
||||
}
|
||||
return log;
|
||||
});
|
||||
return { logs: updatedLogs };
|
||||
});
|
||||
},
|
||||
|
||||
clearLogs: () => {
|
||||
set({ logs: [] });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user