Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 52 additions & 14 deletions src/components/AnalysisPlayground.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { cadenceApi } from "@/lib/cadenceApi";
import { cadenceApi, parseAnalysisError } from "@/lib/cadenceApi";
import {
useStreamingAnalysis,
type StreamStatus,
Expand Down Expand Up @@ -420,19 +420,57 @@ export function AnalysisPlayground() {

{/* ---- Error ---- */}
{stream.error && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
className="p-4 rounded-lg bg-red-500/5 border border-red-500/20 flex items-start gap-3"
>
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-300 mb-1">
Analysis Failed
</p>
<p className="text-sm text-red-200/80">{stream.error}</p>
</div>
</motion.div>
(() => {
const parsedError = parseAnalysisError(stream.error);
return (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
className="p-4 rounded-lg bg-red-500/5 border border-red-500/20"
>
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-semibold text-red-300 mb-1">
{parsedError.title}
</p>
<p className="text-sm text-red-200/80 mb-2">
{parsedError.message}
</p>
{parsedError.suggestion && (
<p className="text-xs text-red-200/60 mb-3 italic">
💡 {parsedError.suggestion}
</p>
)}
<div className="flex gap-2">
{parsedError.isRetryable && (
<button
onClick={() => {
if (stream.analysisType && stream.targetUrl) {
actions.startAnalysis(
stream.analysisType,
stream.targetUrl
);
}
}}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-red-500/20 border border-red-500/30 text-red-300 text-xs font-medium hover:bg-red-500/30 hover:text-red-200 transition-colors"
>
<RotateCcw className="w-3.5 h-3.5" />
Retry
</button>
)}
<button
onClick={handleReset}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-white/70 text-xs font-medium hover:bg-white/10 hover:text-white transition-colors"
>
Try Different URL
</button>
</div>
</div>
</div>
</motion.div>
);
})()
)}

{/* ---- Live detection feed ---- */}
Expand Down
101 changes: 101 additions & 0 deletions src/lib/cadenceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,5 +393,106 @@ class CadenceAPI {
}
}

// ---- Error message parser for user-friendly explanations ---- //

export interface ParsedError {
title: string;
message: string;
suggestion?: string;
isRetryable: boolean;
}

export function parseAnalysisError(errorText: string): ParsedError {
const lower = errorText.toLowerCase();

// Bot protection / 403 Forbidden
if (lower.includes("403") || lower.includes("forbidden")) {
return {
title: "Website Blocked Analysis",
message:
"The website appears to have bot protection enabled (Cloudflare, WAF, or similar) that prevented analysis.",
suggestion:
"Try analyzing a different website, or check if the site is publicly accessible without special headers.",
isRetryable: false,
};
}

// 404 Not Found
if (lower.includes("404") || lower.includes("not found")) {
return {
title: "Website Not Found",
message: "The URL could not be reached or no longer exists.",
suggestion: "Verify the URL is correct and the website is online.",
isRetryable: true,
};
}

// Connection timeout
if (lower.includes("timeout") || lower.includes("timed out")) {
return {
title: "Connection Timeout",
message: "The request took too long and was cancelled.",
suggestion: "The website may be slow or unresponsive. Try again in a moment.",
isRetryable: true,
};
}

// DNS / Network errors
if (
lower.includes("dns") ||
lower.includes("net::") ||
lower.includes("econnrefused") ||
lower.includes("enotfound")
) {
return {
title: "Network Error",
message: "Could not connect to the website due to a network issue.",
suggestion:
"Check if the domain name is correct and the website is online.",
isRetryable: true,
};
}

// TLS / SSL errors
if (lower.includes("ssl") || lower.includes("certificate") || lower.includes("tls")) {
return {
title: "SSL/Certificate Error",
message: "The website has an SSL certificate issue.",
suggestion: "The website's SSL certificate may be invalid or expired.",
isRetryable: false,
};
}

// Rate limiting
if (lower.includes("rate limit") || lower.includes("429")) {
return {
title: "Rate Limited",
message: "Too many requests were sent too quickly.",
suggestion: "Please wait a moment before trying again.",
isRetryable: true,
};
}

// Generic fetch errors
if (lower.includes("failed to fetch")) {
return {
title: "Failed to Fetch",
message:
"Could not fetch the website. This may be due to network issues or website protection.",
suggestion:
"Check your internet connection or try analyzing a different website.",
isRetryable: true,
};
}

// Default fallback
return {
title: "Analysis Failed",
message: errorText || "An unknown error occurred during analysis.",
suggestion: "Please try again or contact support if the issue persists.",
isRetryable: true,
};
}

// Export singleton instance
export const cadenceApi = new CadenceAPI();
Loading