Spaces:
Runtime error
Runtime error
""" | |
UI utility functions - merged from display.py and helpers.py | |
Making the UI component self-contained | |
""" | |
import os | |
import json | |
from datetime import datetime | |
from collections import defaultdict | |
from typing import List, Dict, Any, Tuple | |
import bleach | |
def sanitize_html(text: str, allow_tags: bool = False) -> str: | |
"""Sanitize HTML content to prevent XSS attacks""" | |
if not text: | |
return "" | |
if allow_tags: | |
# Allow basic formatting tags for email bodies | |
allowed_tags = ['p', 'br', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code'] | |
allowed_attrs = {'a': ['href', 'title']} | |
return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, strip=True) | |
else: | |
# For fields like subject, sender name - no HTML allowed | |
return bleach.clean(text, tags=[], attributes={}, strip=True) | |
def get_folder_counts(emails: List[Dict]) -> List[Tuple[str, int]]: | |
"""Extract unique folders and their counts from email data""" | |
folder_counts = defaultdict(int) | |
for email in emails: | |
if 'folder' in email and email['folder']: | |
folder_counts[email['folder']] += 1 | |
else: | |
folder_counts['Inbox'] += 1 | |
return sorted(folder_counts.items()) | |
def get_folder_dropdown_choices(emails: List[Dict]) -> List[str]: | |
"""Get folder choices for dropdown in format 'Folder Name (Count)'""" | |
folder_counts = get_folder_counts(emails) | |
return [f"{folder_name} ({count})" for folder_name, count in folder_counts] | |
def extract_folder_name(folder_choice: str) -> str: | |
"""Extract folder name from dropdown choice format 'Folder Name (Count)'""" | |
if '(' in folder_choice: | |
return folder_choice.split(' (')[0] | |
return folder_choice | |
def format_date(date_string: str) -> str: | |
"""Format ISO date string to readable format""" | |
try: | |
dt = datetime.fromisoformat(date_string.replace('Z', '+00:00')) | |
now = datetime.now() | |
if dt.date() == now.date(): | |
return dt.strftime("%H:%M") | |
elif dt.year == now.year: | |
return dt.strftime("%b %d") | |
else: | |
return dt.strftime("%b %d, %Y") | |
except (ValueError, AttributeError) as e: | |
return date_string | |
def sort_emails(emails: List[Dict], sort_option: str) -> List[Dict]: | |
"""Sort emails based on the selected sort option""" | |
if not emails: | |
return emails | |
def get_email_date(email): | |
try: | |
date_str = email.get('date', '') | |
if date_str: | |
return datetime.fromisoformat(date_str.replace('Z', '+00:00')) | |
else: | |
return datetime(1970, 1, 1) | |
except (ValueError, AttributeError, KeyError) as e: | |
return datetime(1970, 1, 1) | |
if sort_option == "Newest First": | |
return sorted(emails, key=get_email_date, reverse=True) | |
elif sort_option == "Oldest First": | |
return sorted(emails, key=get_email_date, reverse=False) | |
else: | |
return emails | |
def create_email_html(emails: List[Dict], selected_folder: str = "Inbox", expanded_email_id: str = None, compact: bool = True) -> str: | |
"""Create HTML for email list""" | |
folder_counts = dict(get_folder_counts(emails)) | |
clean_folder_name = extract_folder_name(selected_folder) | |
html = """ | |
<style> | |
.email-container { | |
font-family: var(--font-family); | |
font-size: var(--font-size-sm); | |
} | |
.email-list { | |
border: var(--border-width) solid var(--color-gray-light); | |
border-radius: var(--border-radius); | |
overflow: hidden; | |
background: var(--color-white); | |
} | |
.email-row { | |
padding: var(--spacing-2) var(--spacing-3); | |
border-bottom: var(--border-width) solid var(--color-gray-light); | |
cursor: pointer; | |
transition: background-color var(--transition-fast); | |
} | |
.email-row:hover { | |
background-color: var(--color-gray-lighter); | |
} | |
.email-row:last-child { | |
border-bottom: none; | |
} | |
.email-row-expanded { | |
background-color: var(--color-gray-lighter); | |
padding: var(--spacing-3); | |
} | |
.email-content { | |
display: flex; | |
align-items: center; | |
gap: var(--spacing-2); | |
} | |
.email-sender { | |
font-weight: var(--font-weight-medium); | |
color: var(--color-dark); | |
min-width: 120px; | |
max-width: 120px; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
.email-sender-unread { | |
font-weight: var(--font-weight-semibold); | |
} | |
.email-subject { | |
color: var(--color-dark); | |
font-weight: var(--font-weight-medium); | |
} | |
.email-snippet { | |
color: var(--color-gray); | |
} | |
.email-meta { | |
color: var(--color-gray); | |
flex: 1; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
.email-date { | |
color: var(--color-gray); | |
font-size: var(--font-size-xs); | |
min-width: 50px; | |
text-align: right; | |
} | |
.email-label { | |
padding: 2px 6px; | |
border-radius: 12px; | |
font-size: var(--font-size-xs); | |
font-weight: var(--font-weight-medium); | |
margin-left: var(--spacing-1); | |
} | |
.label-unread { | |
background: var(--color-primary); | |
color: var(--color-white); | |
} | |
.label-important { | |
background: var(--color-danger); | |
color: var(--color-white); | |
} | |
.label-starred { | |
background: var(--color-warning); | |
color: var(--color-white); | |
} | |
.email-body { | |
margin-top: var(--spacing-3); | |
padding: var(--spacing-3); | |
background: var(--color-white); | |
border-radius: var(--border-radius-sm); | |
border-left: 3px solid var(--color-primary); | |
line-height: 1.5; | |
} | |
</style> | |
<div class="email-container"> | |
<div class="email-list"> | |
""" | |
for i, email in enumerate(emails): | |
# Email metadata with sanitization | |
email_id = email.get('id', f'email_{i}') | |
sender_name = sanitize_html(email.get('from_name', email.get('from_email', 'Unknown'))[:20]) | |
subject = sanitize_html(email.get('subject', 'No Subject')) | |
snippet = sanitize_html(email.get('snippet', '')) | |
formatted_date = format_date(email.get('date', '')) | |
is_unread = "UNREAD" in email.get("labelIds", []) | |
is_expanded = expanded_email_id == email.get('id') | |
if compact and not is_expanded: | |
# Single line compact format | |
subject_preview = subject[:40] + "..." if len(subject) > 40 else subject | |
snippet_preview = snippet[:30] + "..." if len(snippet) > 30 else snippet | |
# Check for draft indicator | |
has_draft = "DRAFT_PENDING" in email.get("labelIds", []) or "DRAFT_PREVIEW" in email.get("labelIds", []) | |
draft_indicator = '<span class="draft-indicator" title="Draft reply available">📝</span> ' if has_draft else "" | |
# Update labels_html to use CSS classes | |
labels_html = "" | |
if email.get("labelIds"): | |
important_labels = ["UNREAD", "IMPORTANT", "STARRED"] | |
shown_labels = [l for l in email.get("labelIds", []) if l in important_labels][:2] | |
for label in shown_labels: | |
label_short = {"UNREAD": "New", "IMPORTANT": "!", "STARRED": "★"}.get(label, label[:3]) | |
label_class = {"UNREAD": "label-unread", "IMPORTANT": "label-important", "STARRED": "label-starred"}.get(label, "") | |
labels_html += f'<span class="email-label {label_class}">{label_short}</span>' | |
sender_class = "email-sender email-sender-unread" if is_unread else "email-sender" | |
html += f""" | |
<div class="email-row" onclick="handleEmailAction('{email_id}')"> | |
<div class="email-content"> | |
<span class="{sender_class}">{sender_name}</span> | |
<span>•</span> | |
<span class="email-meta"> | |
{draft_indicator}<span class="email-subject">{subject_preview}</span> - <span class="email-snippet">{snippet_preview}</span> | |
</span> | |
{labels_html} | |
<span class="email-date">{formatted_date}</span> | |
</div> | |
</div> | |
""" | |
else: | |
# Expanded view | |
# Update labels_html to use CSS classes | |
labels_html = "" | |
if email.get("labelIds"): | |
important_labels = ["UNREAD", "IMPORTANT", "STARRED"] | |
shown_labels = [l for l in email.get("labelIds", []) if l in important_labels][:2] | |
for label in shown_labels: | |
label_short = {"UNREAD": "New", "IMPORTANT": "!", "STARRED": "★"}.get(label, label[:3]) | |
label_class = {"UNREAD": "label-unread", "IMPORTANT": "label-important", "STARRED": "label-starred"}.get(label, "") | |
labels_html += f'<span class="email-label {label_class}">{label_short}</span>' | |
# Sanitize expanded view data | |
from_name_full = sanitize_html(email.get('from_name', sender_name)) | |
html += f""" | |
<div class="email-row email-row-expanded" onclick="handleEmailAction('{email_id}')"> | |
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-2);"> | |
<span style="font-weight: var(--font-weight-semibold); color: var(--color-dark);">{from_name_full}</span> | |
<div style="display: flex; align-items: center; gap: var(--spacing-2);"> | |
{labels_html} | |
<span class="email-date">{formatted_date}</span> | |
</div> | |
</div> | |
<div style="font-weight: var(--font-weight-medium); color: var(--color-dark); margin-bottom: var(--spacing-2);">{subject}</div> | |
""" | |
if 'body' in email: | |
# Sanitize body content with allowed tags | |
safe_body = sanitize_html(email['body'], allow_tags=True) | |
html += f""" | |
<div class="email-body"> | |
<div style="color: var(--color-dark); white-space: pre-line;">{safe_body}</div> | |
</div> | |
""" | |
# Check for drafts to display | |
if "DRAFT_PENDING" in email.get("labelIds", []) or "DRAFT_PREVIEW" in email.get("labelIds", []): | |
# For now, show a simple draft preview | |
# In production, we'd fetch actual drafts from backend | |
html += f""" | |
<div class="draft-preview" style="margin-top: var(--spacing-4); padding: var(--spacing-3); background-color: #e3f2fd; border-radius: var(--radius-default); border-left: 4px solid #2196f3;"> | |
<h4 style="margin: 0 0 var(--spacing-2) 0; color: #1976d2; font-size: var(--font-size-sm); display: flex; align-items: center;"> | |
<span style="margin-right: var(--spacing-1);">📝</span> Draft Reply Ready | |
</h4> | |
<div style="font-size: var(--font-size-sm); color: var(--color-dark); font-style: italic;"> | |
A personalized draft reply has been prepared for this email. | |
Click to view and edit the full draft. | |
</div> | |
<div style="margin-top: var(--spacing-2);"> | |
<button style="font-size: var(--font-size-xs); padding: 4px 12px; background: #2196f3; color: white; border: none; border-radius: var(--radius-small); cursor: pointer;"> | |
View Draft | |
</button> | |
</div> | |
</div> | |
""" | |
html += """ | |
</div> | |
""" | |
html += """ | |
</div> | |
</div>""" | |
return html | |
def filter_emails(folder: str, search_query: str = "", sort_option: str = "Newest First", emails_source: List[Dict] = None) -> List[Dict]: | |
"""Filter and sort emails based on folder, search query, and sort option""" | |
if emails_source is None: | |
return [] | |
filtered = emails_source | |
clean_folder_name = extract_folder_name(folder) | |
# Filter by folder | |
folder_filtered = [] | |
for email in filtered: | |
if 'folder' in email and email['folder'] == clean_folder_name: | |
folder_filtered.append(email) | |
elif not 'folder' in email and 'INBOX' == clean_folder_name: | |
folder_filtered.append(email) | |
filtered = folder_filtered | |
# Filter by search query | |
if search_query: | |
filtered = [email for email in filtered if | |
search_query.lower() in email.get('from_name', '').lower() or | |
search_query.lower() in email.get('from_email', '').lower() or | |
search_query.lower() in email.get('subject', '').lower() or | |
search_query.lower() in email.get('snippet', '').lower()] | |
# Sort emails | |
filtered = sort_emails(filtered, sort_option) | |
return filtered | |
def create_preview_banner(preview_rules_count: int = 0) -> str: | |
"""Create the preview mode banner HTML""" | |
if preview_rules_count == 0: | |
return "" | |
return f""" | |
<style> | |
#preview-banner {{ | |
background: var(--color-warning); | |
color: var(--color-white); | |
padding: var(--spacing-2) var(--spacing-3); | |
margin-bottom: var(--spacing-2); | |
border-radius: var(--border-radius-sm); | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
font-size: var(--font-size-sm); | |
font-weight: var(--font-weight-medium); | |
box-shadow: var(--shadow-sm); | |
}} | |
#exit-preview-btn {{ | |
background: rgba(255, 255, 255, 0.2); | |
border: 1px solid rgba(255, 255, 255, 0.4); | |
color: var(--color-white); | |
padding: var(--spacing-1) var(--spacing-2); | |
border-radius: var(--border-radius-sm); | |
cursor: pointer; | |
font-size: var(--font-size-xs); | |
font-weight: var(--font-weight-medium); | |
transition: background var(--transition-fast); | |
}} | |
#exit-preview-btn:hover {{ | |
background: rgba(255, 255, 255, 0.3); | |
}} | |
</style> | |
<div id="preview-banner"> | |
<div style="display: flex; align-items: center; gap: var(--spacing-2);"> | |
<span>👁️</span> | |
<span>Preview Mode ({preview_rules_count} rule{'s' if preview_rules_count != 1 else ''})</span> | |
</div> | |
<button id="exit-preview-btn"> | |
Exit Preview | |
</button> | |
</div> | |
""" | |
def create_sleek_rule_card(rule: Dict) -> str: | |
"""Create a single sleek two-row rule card with expandable details""" | |
rule_id = rule.get('rule_id', rule.get('id', '')) | |
print(f"DEBUG ui_utils: Creating card for rule '{rule.get('name')}' with ID: '{rule_id}'") | |
rule_name = sanitize_html(rule.get('name', 'Unnamed Rule')) | |
status = rule.get('status', 'pending') | |
# Determine button set based on status | |
buttons_html = "" | |
if status == 'pending': | |
buttons_html = f""" | |
<button class="rule-btn preview-btn" data-rule-id="{rule_id}">Show affected</button> | |
<button class="rule-btn archive-btn" data-rule-id="{rule_id}">Archive</button> | |
""" | |
elif status == 'preview': | |
buttons_html = f""" | |
<button class="rule-btn accept-btn" data-rule-id="{rule_id}">✓ Accept</button> | |
<button class="rule-btn reject-btn" data-rule-id="{rule_id}">✗ Reject</button> | |
""" | |
elif status == 'accepted': | |
buttons_html = f""" | |
<button class="rule-btn run-btn" data-rule-id="{rule_id}">▶ Run Rule</button> | |
<button class="rule-btn archive-btn" data-rule-id="{rule_id}">Archive</button> | |
""" | |
card_class = f"rule-card-v2 rule-status-{status}" | |
# Build details HTML | |
details_html = "" | |
# Add conditions | |
conditions = rule.get('conditions', []) | |
if conditions: | |
details_html += "<div style='margin-top: 12px;'><strong>Conditions:</strong><ul style='margin: 4px 0; padding-left: 20px;'>" | |
for condition in conditions: | |
field = condition.get('field', '') | |
operator = condition.get('operator', '') | |
value = condition.get('value', '') | |
# Convert operator to readable format | |
op_map = {'contains': 'contains', 'equals': 'equals', 'startswith': 'starts with', 'endswith': 'ends with'} | |
readable_op = op_map.get(operator, operator) | |
details_html += f"<li>{sanitize_html(field)} {readable_op} \"{sanitize_html(str(value))}\"</li>" | |
details_html += "</ul></div>" | |
# Add actions | |
actions = rule.get('actions', []) | |
if actions: | |
details_html += "<div style='margin-top: 8px;'><strong>Actions:</strong><ul style='margin: 4px 0; padding-left: 20px;'>" | |
for action in actions: | |
action_type = action.get('type', '') | |
if action_type == 'move': | |
folder = action.get('parameters', {}).get('folder', 'Unknown') | |
details_html += f"<li>Move to {sanitize_html(folder)} folder</li>" | |
elif action_type == 'label': | |
label = action.get('parameters', {}).get('label', 'Unknown') | |
details_html += f"<li>Add label: {sanitize_html(label)}</li>" | |
elif action_type == 'mark_as_read': | |
details_html += "<li>Mark as read</li>" | |
elif action_type == 'draft_reply': | |
details_html += "<li>Create draft reply</li>" | |
else: | |
details_html += f"<li>{sanitize_html(action_type)}</li>" | |
details_html += "</ul></div>" | |
# Add confidence if available | |
confidence = rule.get('confidence', 0) | |
if confidence > 0: | |
details_html += f"<div style='margin-top: 8px;'><strong>Confidence:</strong> {int(confidence * 100)}%</div>" | |
return f""" | |
<div id="rule-{rule_id}" class="{card_class}" onclick="toggleRuleDetails('{rule_id}')"> | |
<div class="rule-row-1"> | |
<span class="rule-expand-arrow" id="arrow-{rule_id}">▶</span> | |
<span class="rule-name">{rule_name}</span> | |
</div> | |
<div class="rule-row-2"> | |
<div class="rule-actions">{buttons_html}</div> | |
</div> | |
<div class="rule-details" id="details-{rule_id}" style="display: none;"> | |
{details_html} | |
</div> | |
</div> | |
""" | |
def create_interactive_rule_cards(pending_rules: List[Dict], compact: bool = True) -> str: | |
"""Create HTML for interactive rule cards display""" | |
if not pending_rules: | |
return """ | |
<style> | |
.empty-rules { | |
font-family: var(--font-family); | |
padding: var(--spacing-4); | |
text-align: center; | |
color: var(--color-gray); | |
background: var(--color-gray-lighter); | |
border-radius: var(--border-radius); | |
margin: var(--spacing-2); | |
} | |
.empty-rules-icon { | |
font-size: 2rem; | |
margin-bottom: var(--spacing-2); | |
} | |
.empty-rules h4 { | |
margin: var(--spacing-2) 0; | |
color: var(--color-dark); | |
font-size: var(--font-size-base); | |
font-weight: var(--font-weight-semibold); | |
} | |
.empty-rules p { | |
margin: 0; | |
font-size: var(--font-size-sm); | |
} | |
</style> | |
<div class="empty-rules"> | |
<div class="empty-rules-icon">📋</div> | |
<h4>No Rules Yet</h4> | |
<p>Click "Analyze" to get started!</p> | |
</div> | |
""" | |
# Separate active and archived rules | |
active_rules = [r for r in pending_rules if r.get("status") != "rejected"] | |
archived_rules = [r for r in pending_rules if r.get("status") == "rejected"] | |
html = f""" | |
<style> | |
.rules-container {{ | |
font-family: var(--font-family); | |
font-size: var(--font-size-sm); | |
}} | |
.rules-header {{ | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
margin-bottom: var(--spacing-2); | |
}} | |
.rules-title {{ | |
display: flex; | |
align-items: center; | |
gap: var(--spacing-2); | |
}} | |
.rules-title-text {{ | |
font-weight: var(--font-weight-semibold); | |
color: var(--color-dark); | |
}} | |
.archive-toggle {{ | |
font-size: var(--font-size-xs); | |
padding: var(--spacing-1) var(--spacing-2); | |
border: var(--border-width) solid var(--color-gray-light); | |
border-radius: var(--border-radius-sm); | |
background: var(--color-white); | |
cursor: pointer; | |
transition: all var(--transition-fast); | |
font-weight: var(--font-weight-medium); | |
}} | |
.archive-toggle:hover {{ | |
background: var(--color-gray-lighter); | |
border-color: var(--color-gray); | |
}} | |
.rule-card {{ | |
border: var(--border-width) solid var(--color-gray-light); | |
border-radius: var(--border-radius); | |
padding: var(--spacing-2) var(--spacing-3); | |
margin: var(--spacing-2) 0; | |
background: var(--color-white); | |
transition: all var(--transition-fast); | |
box-shadow: var(--shadow-sm); | |
}} | |
.rule-card:hover {{ | |
box-shadow: var(--shadow); | |
transform: translateY(-1px); | |
}} | |
.rule-card-accepted {{ | |
background: #d4edda; | |
border-color: var(--color-success); | |
}} | |
.rule-card-preview {{ | |
background: #fff3cd; | |
border-color: var(--color-warning); | |
}} | |
.rule-content {{ | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
gap: var(--spacing-2); | |
}} | |
.rule-info {{ | |
flex: 1; | |
min-width: 0; | |
cursor: pointer; | |
}} | |
.rule-name-row {{ | |
display: flex; | |
align-items: center; | |
gap: var(--spacing-2); | |
}} | |
.rule-toggle-arrow {{ | |
cursor: pointer; | |
font-size: var(--font-size-sm); | |
width: 15px; | |
text-align: center; | |
color: var(--color-gray); | |
transition: transform var(--transition-fast); | |
}} | |
.rule-name {{ | |
font-weight: var(--font-weight-semibold); | |
color: var(--color-dark); | |
font-size: var(--font-size-sm); | |
}} | |
.rule-summary {{ | |
color: var(--color-gray); | |
font-size: var(--font-size-xs); | |
margin-top: var(--spacing-1); | |
padding-left: 23px; | |
}} | |
.rule-actions {{ | |
display: flex; | |
gap: var(--spacing-1); | |
align-items: center; | |
}} | |
.rule-btn {{ | |
padding: var(--spacing-1) var(--spacing-2); | |
border-radius: var(--border-radius-sm); | |
cursor: pointer; | |
font-size: var(--font-size-xs); | |
font-weight: var(--font-weight-medium); | |
transition: all var(--transition-fast); | |
border: var(--border-width) solid transparent; | |
}} | |
.rule-btn:hover {{ | |
transform: translateY(-1px); | |
box-shadow: var(--shadow-sm); | |
}} | |
.preview-btn {{ | |
border-color: var(--color-primary); | |
background: var(--color-white); | |
color: var(--color-primary); | |
}} | |
.preview-btn:hover {{ | |
background: var(--color-primary); | |
color: var(--color-white); | |
}} | |
.accept-btn {{ | |
background: var(--color-success); | |
color: var(--color-white); | |
}} | |
.accept-btn:hover {{ | |
background: #218838; | |
}} | |
.reject-btn {{ | |
background: var(--color-danger); | |
color: var(--color-white); | |
}} | |
.reject-btn:hover {{ | |
background: #c82333; | |
}} | |
.exit-preview-card-btn {{ | |
border-color: var(--color-warning); | |
background: var(--color-white); | |
color: var(--color-warning); | |
}} | |
.exit-preview-card-btn:hover {{ | |
background: var(--color-warning); | |
color: var(--color-white); | |
}} | |
.rule-details {{ | |
display: none; | |
margin-top: var(--spacing-3); | |
padding: var(--spacing-3); | |
background: rgba(0,0,0,0.02); | |
border-radius: var(--border-radius-sm); | |
font-size: var(--font-size-xs); | |
}} | |
.rule-details-expanded {{ | |
display: block; | |
}} | |
.archived-section {{ | |
margin-top: var(--spacing-4); | |
padding-top: var(--spacing-3); | |
border-top: var(--border-width) solid var(--color-gray-light); | |
}} | |
.archived-rule {{ | |
border: var(--border-width) solid var(--color-gray-light); | |
border-radius: var(--border-radius); | |
padding: var(--spacing-2) var(--spacing-3); | |
margin: var(--spacing-2) 0; | |
background: var(--color-gray-lighter); | |
opacity: 0.7; | |
}} | |
.reactivate-btn {{ | |
padding: var(--spacing-1) var(--spacing-2); | |
border: var(--border-width) solid var(--color-success); | |
background: var(--color-white); | |
color: var(--color-success); | |
border-radius: var(--border-radius-sm); | |
cursor: pointer; | |
font-size: var(--font-size-xs); | |
font-weight: var(--font-weight-medium); | |
transition: all var(--transition-fast); | |
}} | |
.reactivate-btn:hover {{ | |
background: var(--color-success); | |
color: var(--color-white); | |
}} | |
</style> | |
<div class="rules-container"> | |
<!-- Active Rules --> | |
<div style="margin-bottom: var(--spacing-4);"> | |
<div class="rules-header"> | |
<div class="rules-title"> | |
<span style="font-size: 1rem;">📋</span> | |
<span class="rules-title-text">Active Rules ({len(active_rules)})</span> | |
</div> | |
{f'<button onclick="toggleArchived()" class="archive-toggle">{"Hide" if archived_rules else "Show"} Archived ▼</button>' if archived_rules else ''} | |
</div> | |
""" | |
# Active rules - using new sleek cards | |
for rule in active_rules: | |
if rule.get("status") == "rejected": | |
continue | |
html += create_sleek_rule_card(rule) | |
html += """ | |
</div> | |
""" | |
# Archived rules section (if any) | |
if archived_rules: | |
html += f""" | |
<div id="archived-section" style="margin-top: 1rem; display: none;"> | |
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;"> | |
<span style="font-size: 1rem;">🗄️</span> | |
<span style="font-weight: 600; color: #6c757d;">Archived Rules ({len(archived_rules)})</span> | |
</div> | |
""" | |
for rule in archived_rules: | |
rule_id = rule.get('rule_id', rule.get('id', '')) | |
archived_rule_name = sanitize_html(rule.get('name', 'Unnamed Rule')) | |
html += f""" | |
<div class="archived-rule"> | |
<div style="display: flex; align-items: center; justify-content: space-between;"> | |
<span style="color: var(--color-gray); font-size: var(--font-size-sm);">{archived_rule_name}</span> | |
<button class="reactivate-btn" data-rule-id="{rule_id}">Reactivate</button> | |
</div> | |
</div> | |
""" | |
html += """ | |
</div> | |
""" | |
html += """ | |
</div> | |
<script> | |
function toggleArchived() { | |
const section = document.getElementById('archived-section'); | |
if (section) { | |
section.style.display = section.style.display === 'none' ? 'block' : 'none'; | |
} | |
} | |
</script> | |
""" | |
return html | |
def get_preview_rules_count(pending_rules: List[Dict[str, Any]]) -> int: | |
"""Get the number of rules currently in preview status""" | |
return len([rule for rule in pending_rules if rule.get("status") == "preview"]) |