AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
@@ -35,49 +35,113 @@ export const CopilotKitHealthProvider: React.FC<CopilotKitHealthProviderProps> =
|
||||
children,
|
||||
initialHealthStatus = true,
|
||||
}) => {
|
||||
// Persist health status across page reloads to prevent manual form from disappearing
|
||||
const getInitialHealthStatus = (): boolean => {
|
||||
if (typeof window === 'undefined') return initialHealthStatus;
|
||||
try {
|
||||
const saved = localStorage.getItem('copilotkit_health_status');
|
||||
if (saved !== null) {
|
||||
return saved === 'true';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[CopilotKitHealthContext] Failed to read persisted health status:', e);
|
||||
}
|
||||
return initialHealthStatus;
|
||||
};
|
||||
|
||||
const [state, setState] = useState<CopilotKitHealthState>({
|
||||
isHealthy: initialHealthStatus,
|
||||
isHealthy: getInitialHealthStatus(),
|
||||
isChecking: false,
|
||||
lastChecked: null,
|
||||
errorMessage: null,
|
||||
retryCount: 0,
|
||||
isAvailable: initialHealthStatus,
|
||||
isAvailable: getInitialHealthStatus(),
|
||||
});
|
||||
|
||||
const markHealthy = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isHealthy: true,
|
||||
isAvailable: true,
|
||||
errorMessage: null,
|
||||
retryCount: 0,
|
||||
lastChecked: new Date(),
|
||||
}));
|
||||
setState((prev) => {
|
||||
const newState = {
|
||||
...prev,
|
||||
isHealthy: true,
|
||||
isAvailable: true,
|
||||
errorMessage: null,
|
||||
retryCount: 0,
|
||||
lastChecked: new Date(),
|
||||
};
|
||||
// Persist health status to localStorage
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('copilotkit_health_status', 'true');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[CopilotKitHealthContext] Failed to persist health status:', e);
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const markUnhealthy = useCallback((errorMessage?: string) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isHealthy: false,
|
||||
isAvailable: false,
|
||||
errorMessage: errorMessage || 'CopilotKit is unavailable',
|
||||
lastChecked: new Date(),
|
||||
retryCount: prev.retryCount + 1,
|
||||
}));
|
||||
setState((prev) => {
|
||||
const newState = {
|
||||
...prev,
|
||||
isHealthy: false,
|
||||
isAvailable: false,
|
||||
errorMessage: errorMessage || 'CopilotKit is unavailable',
|
||||
lastChecked: new Date(),
|
||||
retryCount: prev.retryCount + 1,
|
||||
};
|
||||
// Persist health status to localStorage
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('copilotkit_health_status', 'false');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[CopilotKitHealthContext] Failed to persist health status:', e);
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Listen for CopilotKit error events from App.tsx
|
||||
React.useEffect(() => {
|
||||
const handleCopilotKitError = (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
const { errorMessage, isFatal } = customEvent.detail || {};
|
||||
const { errorMessage, isFatal, error } = customEvent.detail || {};
|
||||
|
||||
// Always mark as unhealthy for fatal errors (CORS, SSL, 403, etc.)
|
||||
if (isFatal) {
|
||||
console.warn('[CopilotKitHealthContext] Fatal CopilotKit error detected:', errorMessage);
|
||||
markUnhealthy(errorMessage || 'CopilotKit fatal error');
|
||||
} else {
|
||||
// For transient errors, just log but don't mark as unhealthy immediately
|
||||
// Let the health check determine if it's truly down
|
||||
console.warn('CopilotKit transient error:', errorMessage);
|
||||
// Check error details for CORS/network errors even if not marked as fatal
|
||||
// Safely check error strings to avoid "Cannot read properties of undefined" errors
|
||||
const errorMsg = error?.message || '';
|
||||
const errorMsgStr = typeof errorMsg === 'string' ? errorMsg.toLowerCase() : String(errorMsg || '').toLowerCase();
|
||||
const messageStr = typeof errorMessage === 'string' ? errorMessage.toLowerCase() : String(errorMessage || '').toLowerCase();
|
||||
const errorStr = errorMsgStr || messageStr || '';
|
||||
|
||||
if (errorStr && (
|
||||
errorStr.includes('cors') ||
|
||||
errorStr.includes('failed to fetch') ||
|
||||
errorStr.includes('networkerror') ||
|
||||
errorStr.includes('network') ||
|
||||
errorStr.includes('cannot read properties')
|
||||
)) {
|
||||
console.warn('[CopilotKitHealthContext] CORS/network error detected:', errorMessage);
|
||||
markUnhealthy(errorMessage || 'CopilotKit network error');
|
||||
} else if (error?.response?.status === 504 || error?.response?.status === 502 || error?.response?.status === 500) {
|
||||
// Gateway timeout, bad gateway, or server error - mark as unavailable
|
||||
console.warn('[CopilotKitHealthContext] Gateway/server error detected:', errorMessage);
|
||||
markUnhealthy(errorMessage || 'CopilotKit gateway error');
|
||||
} else if (error?.error?.statusCode === 500 || error?.error?.code === 'UNKNOWN') {
|
||||
// Internal CopilotKit errors - mark as unavailable
|
||||
console.warn('[CopilotKitHealthContext] CopilotKit internal error detected:', errorMessage);
|
||||
markUnhealthy(errorMessage || 'CopilotKit internal error');
|
||||
} else {
|
||||
// For other transient errors, just log but don't mark as unhealthy immediately
|
||||
// Let the health check determine if it's truly down
|
||||
console.warn('[CopilotKitHealthContext] Transient CopilotKit error:', errorMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,41 +176,62 @@ export const CopilotKitHealthProvider: React.FC<CopilotKitHealthProviderProps> =
|
||||
|
||||
// Try to check CopilotKit status endpoint
|
||||
// This is a lightweight check that doesn't require full CopilotKit initialization
|
||||
const response = await fetch('https://api.cloud.copilotkit.ai/ciu', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-copilotcloud-public-api-key': apiKey.trim(),
|
||||
},
|
||||
// Use a short timeout to avoid blocking
|
||||
signal: AbortSignal.timeout(3000),
|
||||
});
|
||||
// Use AbortController for timeout (more compatible than AbortSignal.timeout)
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.cloud.copilotkit.ai/ciu', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-copilotcloud-public-api-key': apiKey.trim(),
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (response.ok) {
|
||||
markHealthy();
|
||||
} else {
|
||||
// Provide more specific error messages based on status code
|
||||
if (response.status === 401) {
|
||||
markUnhealthy('CopilotKit API key is invalid or unauthorized');
|
||||
if (response.ok) {
|
||||
markHealthy();
|
||||
} else {
|
||||
markUnhealthy(`CopilotKit status check failed: ${response.status}`);
|
||||
// Provide more specific error messages based on status code
|
||||
if (response.status === 401) {
|
||||
markUnhealthy('CopilotKit API key is invalid or unauthorized');
|
||||
} else if (response.status === 429) {
|
||||
markUnhealthy('CopilotKit rate limit exceeded');
|
||||
} else if (response.status >= 500) {
|
||||
markUnhealthy(`CopilotKit server error: ${response.status}`);
|
||||
} else {
|
||||
markUnhealthy(`CopilotKit status check failed: ${response.status}`);
|
||||
}
|
||||
}
|
||||
} catch (fetchError: any) {
|
||||
clearTimeout(timeoutId);
|
||||
throw fetchError;
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Handle various error types
|
||||
let errorMsg = 'CopilotKit health check failed';
|
||||
let isCorsError = false;
|
||||
|
||||
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
|
||||
errorMsg = 'CopilotKit health check timed out';
|
||||
} else if (error.message?.includes('CORS')) {
|
||||
} else if (error.message?.includes('CORS') || error.message?.includes('cors')) {
|
||||
errorMsg = 'CopilotKit CORS error - service may be unavailable';
|
||||
isCorsError = true;
|
||||
} else if (error.message?.includes('Failed to fetch') || error.message?.includes('NetworkError')) {
|
||||
// Failed to fetch often indicates CORS or network issues
|
||||
errorMsg = 'CopilotKit network error - service may be down or blocked';
|
||||
isCorsError = true; // Treat as potentially unavailable
|
||||
} else if (error.message?.includes('certificate') || error.message?.includes('SSL')) {
|
||||
errorMsg = 'CopilotKit SSL certificate error';
|
||||
} else if (error.message?.includes('network') || error.message?.includes('Failed to fetch')) {
|
||||
} else if (error.message?.includes('network') || error.message?.includes('Network')) {
|
||||
errorMsg = 'CopilotKit network error - service may be down';
|
||||
} else {
|
||||
errorMsg = error.message || 'Unknown error checking CopilotKit health';
|
||||
}
|
||||
|
||||
console.warn('[CopilotKitHealthContext] Health check failed:', errorMsg, error);
|
||||
markUnhealthy(errorMsg);
|
||||
} finally {
|
||||
setState((prev) => ({ ...prev, isChecking: false }));
|
||||
|
||||
@@ -96,9 +96,40 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait a moment to ensure auth token getter is installed
|
||||
// This prevents 401 errors during app initialization
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
// Wait for authentication to be ready
|
||||
// The apiClient interceptor needs authTokenGetter to be set by TokenInstaller
|
||||
// Wait up to 2 seconds for token getter to be installed (TokenInstaller runs in App.tsx)
|
||||
let authReady = false;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 20; // 20 * 100ms = 2 seconds max wait
|
||||
|
||||
while (attempts < maxAttempts && !authReady) {
|
||||
// Wait for TokenInstaller to set the authTokenGetter in api/client.ts
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Check if user_id exists (indicates user is authenticated)
|
||||
const storedUserId = localStorage.getItem('user_id');
|
||||
if (storedUserId && storedUserId !== 'anonymous') {
|
||||
// After a few attempts, assume token getter should be ready
|
||||
// The apiClient interceptor will add the token if authTokenGetter is set
|
||||
if (attempts >= 5) { // After 500ms, proceed with the request
|
||||
authReady = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// No user_id means user is not authenticated, exit early
|
||||
console.log('SubscriptionContext: No user_id found, user not authenticated');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!authReady) {
|
||||
console.warn('SubscriptionContext: Auth token getter may not be ready, but proceeding with request. apiClient will handle 401 gracefully.');
|
||||
// Continue anyway - apiClient interceptor will handle missing token gracefully
|
||||
}
|
||||
|
||||
console.log('SubscriptionContext: Checking subscription for user:', userId);
|
||||
const response = await apiClient.get(`/api/subscription/status/${userId}`);
|
||||
@@ -304,12 +335,25 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
// Check if it's a subscription-related error
|
||||
const status = error.response?.status;
|
||||
|
||||
console.log('SubscriptionContext: globalSubscriptionErrorHandler called', {
|
||||
status,
|
||||
hasResponse: !!error.response,
|
||||
dataKeys: error.response?.data ? Object.keys(error.response.data) : null,
|
||||
data: error.response?.data
|
||||
});
|
||||
|
||||
if (status === 429 || status === 402) {
|
||||
const now = Date.now();
|
||||
|
||||
// Check if this is a usage limit error (status 429) vs subscription expired (402)
|
||||
let errorData = error.response?.data || {};
|
||||
|
||||
console.log('SubscriptionContext: Processing subscription error', {
|
||||
originalErrorData: errorData,
|
||||
isArray: Array.isArray(errorData),
|
||||
hasDetail: errorData.detail !== undefined
|
||||
});
|
||||
|
||||
// If errorData is an array, extract the first element (common FastAPI response format)
|
||||
if (Array.isArray(errorData)) {
|
||||
errorData = errorData[0] || {};
|
||||
@@ -317,10 +361,18 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
|
||||
// CRITICAL: FastAPI wraps HTTPException detail in a 'detail' field
|
||||
// If errorData has a 'detail' field, extract it (this is the actual error data)
|
||||
// BUT: JSONResponse returns data directly, not wrapped in 'detail'
|
||||
if (errorData.detail && typeof errorData.detail === 'object') {
|
||||
errorData = errorData.detail;
|
||||
}
|
||||
|
||||
console.log('SubscriptionContext: Processed errorData', {
|
||||
errorData,
|
||||
hasUsageInfo: !!errorData.usage_info,
|
||||
provider: errorData.provider,
|
||||
message: errorData.message
|
||||
});
|
||||
|
||||
// Check for usage_info in various possible locations (now that we've unwrapped FastAPI detail)
|
||||
const usageInfo = errorData.usage_info ||
|
||||
(errorData.current_calls !== undefined ? errorData : null) ||
|
||||
|
||||
Reference in New Issue
Block a user