diff --git a/src/backend/core/tools.py b/src/backend/core/tools.py index 6dee7d9..3e682a2 100644 --- a/src/backend/core/tools.py +++ b/src/backend/core/tools.py @@ -18,14 +18,17 @@ async def fetch_website_content( url: str, ) -> str: """ - Fetches the HTML content of the given URL via httpx and beautifulsoup + Fetches the HTML content of the given URL via httpx and beautifulsoup. + Returns error message on failure instead of raising exception. :param _ctx: RunContext[SwotAgentDeps] :param url: str - :return: str + :return: str - content or error message """ logger.info(f"Fetching website content for: {url}") - async with httpx.AsyncClient(follow_redirects=True) as http_client: + async with httpx.AsyncClient( + follow_redirects=True, timeout=30.0 + ) as http_client: try: response = await http_client.get(url) response.raise_for_status() @@ -33,9 +36,29 @@ async def fetch_website_content( content = soup(html_content, "html.parser") text_content = content.get_text(separator=" ", strip=True) return text_content - except httpx.HTTPError as e: - logger.info(f"Request failed: {e}") - raise + except httpx.HTTPStatusError as e: + # Handle HTTP errors gracefully (403, 404, 500, etc.) + error_msg = ( + f"Unable to fetch {url}: HTTP {e.response.status_code} " + f"({e.response.reason_phrase}). " + f"The site may be blocking automated requests or require authentication." + ) + logger.warning(error_msg) + return error_msg + except httpx.TimeoutException: + error_msg = f"Request to {url} timed out after 30 seconds." + logger.warning(error_msg) + return error_msg + except httpx.RequestError as e: + # Handle connection errors, invalid URLs, etc. + error_msg = f"Failed to connect to {url}: {type(e).__name__}" + logger.warning(f"{error_msg} - {e}") + return error_msg + except Exception as e: + # Catch any other unexpected errors + error_msg = f"Unexpected error fetching {url}: {type(e).__name__}" + logger.error(f"{error_msg} - {e}") + return error_msg @swot_agent.tool(prepare=report_tool_usage) diff --git a/src/frontend/static/js/app.js b/src/frontend/static/js/app.js index 9833c2f..a4f2ce4 100644 --- a/src/frontend/static/js/app.js +++ b/src/frontend/static/js/app.js @@ -188,6 +188,33 @@ }); } + /** + * Monitor for status updates arrival + */ + function monitorStatusUpdates() { + const statusBox = document.getElementById('status'); + if (!statusBox) return; + + // Watch for first status update to hide overlay + const statusObserver = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === 'childList' && statusBox.innerHTML.trim().length > 0) { + // First status update received - hide spinner overlay + const spinner = document.getElementById('spinner'); + if (spinner && !spinner.classList.contains('is-hidden')) { + spinner.classList.add('is-hidden'); + stopLoadingMessages(); + } + } + }); + }); + + statusObserver.observe(statusBox, { + childList: true, + subtree: true + }); + } + /** * Monitor for analysis completion */ @@ -202,7 +229,7 @@ // Analysis complete stopLoadingMessages(); - // Hide spinner + // Hide spinner (just in case) const spinner = document.getElementById('spinner'); if (spinner) { spinner.classList.add('is-hidden'); @@ -277,6 +304,7 @@ */ function initialize() { initializeForm(); + monitorStatusUpdates(); monitorAnalysisCompletion(); initializeButtonEffects(); initializeSmoothScrolling(); diff --git a/src/frontend/templates/home.html b/src/frontend/templates/home.html index c4d0a44..69bcba5 100644 --- a/src/frontend/templates/home.html +++ b/src/frontend/templates/home.html @@ -89,6 +89,12 @@ hx-get={{ url_for('get_status') }} hx-trigger="load, every 1s" hx-swap="innerHTML" + hx-on:after-request=" + if(this.innerHTML.trim().length > 0) { + const spinner = document.querySelector('#spinner'); + if (spinner) spinner.classList.add('is-hidden'); + } + " style="display: none">