From ce76f958f74c52d33d5383a2d331bafa6f4a2fac Mon Sep 17 00:00:00 2001 From: Francis Secada Date: Wed, 4 Feb 2026 14:36:47 -0500 Subject: [PATCH] refactor: convert status updates to Jinjax component architecture Replaced raw HTML string concatenation with proper Jinjax components following the project's established component patterns. Component Architecture: - StatusItem.jinja: Individual status message component * Accepts: message, is_last, result, use_oob * Determines status type (error/loading/complete/info) automatically * Renders with appropriate icons and styling * Optional HTMX OOB swap for dynamic appending - StatusTimeline.jinja: Container component for status items * Provides semantic HTML structure with ARIA labels * Optional HTMX OOB swap for initial insertion * Children appended via HTMX to #status-timeline Backend Changes: - Removed raw HTML string templates (anti-pattern) - Use catalog.render() for all component rendering - First poll: Render StatusTimeline + StatusItem components with OOB - Subsequent polls: Render only new StatusItem components with OOB - Clean separation of concerns (backend = logic, Jinjax = presentation) Frontend Changes: - Changed #status div to hx-swap="none" (all updates via OOB) - Removed old status.html and status_item.html templates - StatusItem and StatusTimeline now in components/snippets/ Benefits: - No raw HTML in Python code - Reusable Jinjax components - Consistent with project patterns - Easier to maintain and test - Clean separation of presentation and logic Co-Authored-By: Claude Sonnet 4.5 --- src/backend/site/router.py | 53 ++++++++++++------- .../snippets/StatusItem.jinja} | 16 ++++-- .../components/snippets/StatusTimeline.jinja | 22 ++++++++ src/frontend/templates/home.html | 2 +- src/frontend/templates/status.html | 48 ----------------- 5 files changed, 71 insertions(+), 70 deletions(-) rename src/frontend/templates/{status_item.html => components/snippets/StatusItem.jinja} (68%) create mode 100644 src/frontend/templates/components/snippets/StatusTimeline.jinja delete mode 100644 src/frontend/templates/status.html diff --git a/src/backend/site/router.py b/src/backend/site/router.py index 5da2136..4e4bd67 100644 --- a/src/backend/site/router.py +++ b/src/backend/site/router.py @@ -94,47 +94,64 @@ async def analyze_url( @user_frontend.get("/status", response_class=HTMLResponse) async def get_status(request: Request): """ - Returns new status messages since last poll (incremental updates) - First poll returns all messages + container, subsequent polls return - only new items with HTMX OOB swap + Returns new status messages since last poll using pure OOB swaps. + First poll returns the container + initial messages as OOB. + Subsequent polls return only new messages as OOB. + Uses Jinjax components for rendering. :param request: :return: """ session_id = request.session.get("analysis_id") if not session_id: - return templates.TemplateResponse( - "status.html", {"request": request, "messages": [], "result": False} - ) + return HTMLResponse(content="", status_code=200) all_messages = status_store.get(session_id, []) last_index = last_message_index.get(session_id, 0) result = ANALYSIS_COMPLETE_MESSAGE in all_messages - # First poll: return full container with all messages - if last_index == 0: - last_message_index[session_id] = len(all_messages) - return templates.TemplateResponse( - "status.html", - {"request": request, "messages": all_messages, "result": result}, - ) + # No new messages, return empty response + if last_index > 0 and last_index >= len(all_messages): + return HTMLResponse(content="", status_code=200) - # Subsequent polls: return only new messages with OOB swap + # Determine which messages to send new_messages = all_messages[last_index:] last_message_index[session_id] = len(all_messages) if not new_messages: - # No new messages, return empty response return HTMLResponse(content="", status_code=200) - # Render new items with OOB swap + # First poll: return container + messages with OOB + if last_index == 0: + # Render StatusTimeline container with OOB + container_html = catalog.render("StatusTimeline", use_oob=True) + + # Render all initial StatusItem components with OOB + items_html = "" + for idx, message in enumerate(new_messages): + is_last = idx == len(new_messages) - 1 + item = catalog.render( + "StatusItem", + message=message, + is_last=is_last, + result=result, + use_oob=True, + ) + items_html += item + + return HTMLResponse( + content=container_html + items_html, status_code=200 + ) + + # Subsequent polls: return only new StatusItem components with OOB swap items_html = "" for idx, message in enumerate(new_messages): is_last = idx == len(new_messages) - 1 - item = templates.get_template("status_item.html").render( + item = catalog.render( + "StatusItem", message=message, is_last=is_last, result=result, - request=request, + use_oob=True, ) items_html += item diff --git a/src/frontend/templates/status_item.html b/src/frontend/templates/components/snippets/StatusItem.jinja similarity index 68% rename from src/frontend/templates/status_item.html rename to src/frontend/templates/components/snippets/StatusItem.jinja index 698502d..1f092e4 100644 --- a/src/frontend/templates/status_item.html +++ b/src/frontend/templates/components/snippets/StatusItem.jinja @@ -1,14 +1,24 @@ -{# Single status item template for HTMX OOB swaps #} +{# def + message: str, + is_last: bool = False, + result: bool = False, + use_oob: bool = True +#} + +{# Determine status type and styling #} {% set is_error = message.startswith('Error:') %} {% set is_loading = is_last and not result %} {% set is_tool_message = message.startswith('Using tool') %} {% set is_complete = "complete" in message.lower() or "done" in message.lower() %} {% set status_label = 'Error' if is_error else ('Complete' if is_complete else ('In Progress' if is_loading else ('Processing' if is_tool_message else 'Status'))) %} +{% set status_class = 'status-item--error' if is_error else ('status-item--success' if is_complete else ('status-item--loading' if is_loading else 'status-item--info')) %} -
+ {% if use_oob %}hx-swap-oob="beforeend:#status-timeline" + {% endif %}>