generated from Templates/Docker_Image
feat: initial release, refs NOISSUE
This commit is contained in:
435
ai_software_factory/agents/ui_manager.py
Normal file
435
ai_software_factory/agents/ui_manager.py
Normal file
@@ -0,0 +1,435 @@
|
||||
"""UI manager for web dashboard with audit trail display."""
|
||||
|
||||
import json
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class UIManager:
|
||||
"""Manages UI data and updates with audit trail display."""
|
||||
|
||||
def __init__(self, project_id: str):
|
||||
"""Initialize UI manager."""
|
||||
self.project_id = project_id
|
||||
self.ui_data = {
|
||||
"project_id": project_id,
|
||||
"status": "initialized",
|
||||
"progress": 0,
|
||||
"message": "Ready",
|
||||
"snapshots": [],
|
||||
"features": []
|
||||
}
|
||||
|
||||
def update_status(self, status: str, progress: int, message: str) -> None:
|
||||
"""Update UI status."""
|
||||
self.ui_data["status"] = status
|
||||
self.ui_data["progress"] = progress
|
||||
self.ui_data["message"] = message
|
||||
|
||||
def add_snapshot(self, data: str, created_at: Optional[str] = None) -> None:
|
||||
"""Add a snapshot of UI data."""
|
||||
snapshot = {
|
||||
"data": data,
|
||||
"created_at": created_at or self._get_current_timestamp()
|
||||
}
|
||||
self.ui_data.setdefault("snapshots", []).append(snapshot)
|
||||
|
||||
def add_feature(self, feature: str) -> None:
|
||||
"""Add a feature tag."""
|
||||
self.ui_data.setdefault("features", []).append(feature)
|
||||
|
||||
def _get_current_timestamp(self) -> str:
|
||||
"""Get current timestamp in ISO format."""
|
||||
from datetime import datetime
|
||||
return datetime.now().isoformat()
|
||||
|
||||
def get_ui_data(self) -> dict:
|
||||
"""Get current UI data."""
|
||||
return self.ui_data
|
||||
|
||||
def _escape_html(self, text: str) -> str:
|
||||
"""Escape HTML special characters for safe display."""
|
||||
if text is None:
|
||||
return ""
|
||||
safe_chars = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}
|
||||
return ''.join(safe_chars.get(c, c) for c in str(text))
|
||||
|
||||
def render_dashboard(self, audit_trail: Optional[List[dict]] = None,
|
||||
actions: Optional[List[dict]] = None,
|
||||
logs: Optional[List[dict]] = None) -> str:
|
||||
"""Render dashboard HTML with audit trail and history display."""
|
||||
|
||||
# Format logs for display
|
||||
logs_html = ""
|
||||
if logs:
|
||||
for log in logs:
|
||||
level = log.get("level", "INFO")
|
||||
message = self._escape_html(log.get("message", ""))
|
||||
timestamp = self._escape_html(log.get("timestamp", ""))
|
||||
|
||||
if level == "ERROR":
|
||||
level_class = "error"
|
||||
else:
|
||||
level_class = "info"
|
||||
|
||||
logs_html += f"""
|
||||
<div class="log-item">
|
||||
<span class="timestamp">{timestamp}</span>
|
||||
<span class="log-level {level_class}">[{level}]</span>
|
||||
<span>{message}</span>
|
||||
</div>"""
|
||||
|
||||
# Format audit trail for display
|
||||
audit_html = ""
|
||||
if audit_trail:
|
||||
for audit in audit_trail:
|
||||
action = audit.get("action", "")
|
||||
actor = self._escape_html(audit.get("actor", ""))
|
||||
timestamp = self._escape_html(audit.get("timestamp", ""))
|
||||
details = self._escape_html(audit.get("details", ""))
|
||||
metadata = audit.get("metadata", {})
|
||||
action_type = audit.get("action_type", "")
|
||||
|
||||
# Color classes for action types
|
||||
action_color = action_type.lower() if action_type else "neutral"
|
||||
|
||||
audit_html += f"""
|
||||
<div class="audit-item">
|
||||
<div class="audit-header">
|
||||
<span class="audit-action {action_color}">
|
||||
{self._escape_html(action)}
|
||||
</span>
|
||||
<span class="audit-actor">{actor}</span>
|
||||
<span class="audit-time">{timestamp}</span>
|
||||
</div>
|
||||
<div class="audit-details">{details}</div>
|
||||
{f'<div class="audit-metadata">{json.dumps(metadata)}</div>' if metadata else ''}
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Format actions for display
|
||||
actions_html = ""
|
||||
if actions:
|
||||
for action in actions:
|
||||
action_type = action.get("action_type", "")
|
||||
description = self._escape_html(action.get("description", ""))
|
||||
actor_name = self._escape_html(action.get("actor_name", ""))
|
||||
actor_type = action.get("actor_type", "")
|
||||
timestamp = self._escape_html(action.get("timestamp", ""))
|
||||
|
||||
actions_html += f"""
|
||||
<div class="action-item">
|
||||
<div class="action-type">{self._escape_html(action_type)}</div>
|
||||
<div class="action-description">{description}</div>
|
||||
<div class="action-actor">{actor_type}: {actor_name}</div>
|
||||
<div class="action-time">{timestamp}</div>
|
||||
</div>"""
|
||||
|
||||
# Format snapshots for display
|
||||
snapshots_html = ""
|
||||
snapshots = self.ui_data.get("snapshots", [])
|
||||
if snapshots:
|
||||
for snapshot in snapshots:
|
||||
data = snapshot.get("data", "")
|
||||
created_at = snapshot.get("created_at", "")
|
||||
snapshots_html += f"""
|
||||
<div class="snapshot-item">
|
||||
<div class="snapshot-time">{created_at}</div>
|
||||
<pre class="snapshot-data">{data}</pre>
|
||||
</div>"""
|
||||
|
||||
# Build features HTML
|
||||
features_html = ""
|
||||
features = self.ui_data.get("features", [])
|
||||
if features:
|
||||
feature_tags = []
|
||||
for feat in features:
|
||||
feature_tags.append(f'<span class="feature-tag">{self._escape_html(feat)}</span>')
|
||||
features_html = f'<div class="features">{"".join(feature_tags)}</div>'
|
||||
|
||||
# Build project header HTML
|
||||
project_id_escaped = self._escape_html(self.ui_data.get('project_id', 'Project'))
|
||||
status = self.ui_data.get('status', 'initialized')
|
||||
|
||||
# Determine empty state message
|
||||
empty_state_message = ""
|
||||
if not audit_trail and not actions and not snapshots_html:
|
||||
empty_state_message = 'No audit trail entries available'
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Software Factory Dashboard</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}}
|
||||
h1 {{
|
||||
color: #333;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 2rem;
|
||||
}}
|
||||
h2 {{
|
||||
color: #444;
|
||||
margin: 2rem 0 1rem;
|
||||
font-size: 1.5rem;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 0.5rem;
|
||||
}}
|
||||
.project {{
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
.project-header {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
.project-name {{
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}}
|
||||
.status-badge {{
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
}}
|
||||
.status-badge.running {{ background: #fff3cd; color: #856404; }}
|
||||
.status-badge.completed {{ background: #d4edda; color: #155724; }}
|
||||
.status-badge.error {{ background: #f8d7da; color: #721c24; }}
|
||||
.status-badge.initialized {{ background: #e2e3e5; color: #383d41; }}
|
||||
.progress-bar {{
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: #e9ecef;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
}}
|
||||
.progress-fill {{
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
transition: width 0.5s ease;
|
||||
}}
|
||||
.message {{
|
||||
color: #495057;
|
||||
margin: 0.5rem 0;
|
||||
}}
|
||||
.logs {{
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}}
|
||||
.log-item {{
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}}
|
||||
.log-item:last-child {{ border-bottom: none; }}
|
||||
.timestamp {{
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
}}
|
||||
.log-level {{
|
||||
font-weight: bold;
|
||||
margin-right: 0.5rem;
|
||||
}}
|
||||
.log-level.info {{ color: #28a745; }}
|
||||
.log-level.error {{ color: #dc3545; }}
|
||||
.features {{
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}}
|
||||
.feature-tag {{
|
||||
background: #e7f3ff;
|
||||
color: #0066cc;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
}}
|
||||
.audit-section {{
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}}
|
||||
.audit-item {{
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
.audit-header {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}}
|
||||
.audit-action {{
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.audit-action.CREATE {{ background: #d4edda; color: #155724; }}
|
||||
.audit-action.UPDATE {{ background: #cce5ff; color: #004085; }}
|
||||
.audit-action.DELETE {{ background: #f8d7da; color: #721c24; }}
|
||||
.audit-action.PROMPT {{ background: #d1ecf1; color: #0c5460; }}
|
||||
.audit-action.COMMIT {{ background: #fff3cd; color: #856404; }}
|
||||
.audit-action.PR_CREATED {{ background: #d4edda; color: #155724; }}
|
||||
.audit-action.neutral {{ background: #e9ecef; color: #495057; }}
|
||||
.audit-actor {{
|
||||
background: #e9ecef;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}}
|
||||
.audit-time {{
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
}}
|
||||
.audit-details {{
|
||||
color: #495057;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}}
|
||||
.audit-metadata {{
|
||||
background: #f1f3f5;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: monospace;
|
||||
margin-top: 0.5rem;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}}
|
||||
.action-item {{
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
.action-type {{
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
font-size: 0.9rem;
|
||||
}}
|
||||
.action-description {{
|
||||
color: #495057;
|
||||
margin: 0.5rem 0;
|
||||
}}
|
||||
.action-actor {{
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
}}
|
||||
.snapshot-section {{
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}}
|
||||
.snapshot-item {{
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
.snapshot-time {{
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
.snapshot-data {{
|
||||
background: #f8f9fa;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}}
|
||||
.empty-state {{
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
padding: 2rem;
|
||||
}}
|
||||
@media (max-width: 768px) {{
|
||||
.container {{
|
||||
padding: 1rem;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 1.5rem;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>AI Software Factory Dashboard</h1>
|
||||
|
||||
<div class="project">
|
||||
<div class="project-header">
|
||||
<span class="project-name">{project_id_escaped}</span>
|
||||
<span class="status-badge {status}">
|
||||
{status.upper()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {self.ui_data.get('progress', 0)}%;"></div>
|
||||
</div>
|
||||
|
||||
<div class="message">{self._escape_html(self.ui_data.get('message', 'No message'))}</div>
|
||||
|
||||
{f'<div class="logs" id="logs">{logs_html}</div>' if logs else '<div class="empty-state">No logs available</div>'}
|
||||
|
||||
{features_html}
|
||||
</div>
|
||||
|
||||
{f'<div class="audit-section"><h2>Audit Trail</h2>{audit_html}</div>' if audit_html else ''}
|
||||
|
||||
{f'<div class="action-section"><h2>User Actions</h2>{actions_html}</div>' if actions_html else ''}
|
||||
|
||||
{f'<div class="snapshot-section"><h2>UI Snapshots</h2>{snapshots_html}</div>' if snapshots_html else ''}
|
||||
|
||||
{empty_state_message}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
Reference in New Issue
Block a user