mirror of
https://github.com/fsecada01/Pygentic-AI.git
synced 2026-05-11 19:54:59 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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')) %}
|
||||
|
||||
<article class="status-item {% if is_error %}status-item--error{% elif is_complete %}status-item--success{% elif is_loading %}status-item--loading{% else %}status-item--info{% endif %}"
|
||||
{# Render status item with optional OOB swap #}
|
||||
<article class="status-item {{ status_class }}"
|
||||
role="article"
|
||||
aria-label="{{ status_label }}: {{ message }}"
|
||||
hx-swap-oob="beforeend:#status-timeline">
|
||||
{% if use_oob %}hx-swap-oob="beforeend:#status-timeline"
|
||||
{% endif %}>
|
||||
<div class="status-item__indicator"
|
||||
aria-hidden="true">
|
||||
{% if is_error %}
|
||||
@ -0,0 +1,22 @@
|
||||
{# def
|
||||
use_oob: bool = True
|
||||
#}
|
||||
|
||||
{# Status timeline container - can be rendered with or without OOB swap #}
|
||||
<section class="section"
|
||||
id="status-container"
|
||||
role="region"
|
||||
aria-label="Analysis Progress"
|
||||
aria-live="polite"
|
||||
{% if use_oob %}hx-swap-oob="beforeend:#status"
|
||||
{% endif %}>
|
||||
<div class="container"
|
||||
style="max-width: 800px;">
|
||||
<div class="status-timeline"
|
||||
id="status-timeline"
|
||||
role="feed"
|
||||
aria-label="Status updates">
|
||||
{# Child StatusItem components will be appended here via HTMX OOB swaps #}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -108,7 +108,7 @@
|
||||
id="status"
|
||||
hx-get={{ url_for('get_status') }}
|
||||
hx-trigger="every 1s[!document.querySelector('#result-container')]"
|
||||
hx-swap="innerHTML"
|
||||
hx-swap="none"
|
||||
@htmx:after-request="
|
||||
const response = $event.detail.xhr.response.trim();
|
||||
if (response && response.length > 0) {
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
{% if messages %}
|
||||
<section class="section"
|
||||
id="status-container"
|
||||
role="region"
|
||||
aria-label="Analysis Progress"
|
||||
aria-live="polite">
|
||||
<div class="container"
|
||||
style="max-width: 800px;">
|
||||
<div class="status-timeline"
|
||||
id="status-timeline"
|
||||
role="feed"
|
||||
aria-label="Status updates">
|
||||
{% for message in messages %}
|
||||
{% set is_error = message.startswith('Error:') %}
|
||||
{% set is_loading = loop.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'))) %}
|
||||
|
||||
<article class="status-item {% if is_error %}status-item--error{% elif is_complete %}status-item--success{% elif is_loading %}status-item--loading{% else %}status-item--info{% endif %}"
|
||||
role="article"
|
||||
aria-label="{{ status_label }}: {{ message }}">
|
||||
<div class="status-item__indicator"
|
||||
aria-hidden="true">
|
||||
{% if is_error %}
|
||||
<i class="fas fa-circle-xmark"></i>
|
||||
{% elif is_complete %}
|
||||
<i class="fas fa-circle-check"></i>
|
||||
{% elif is_loading %}
|
||||
<i class="fas fa-circle-notch fa-spin"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-circle"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="status-item__content">
|
||||
<div class="status-item__header">
|
||||
{{ status_label }}
|
||||
</div>
|
||||
<div class="status-item__message">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user