Files
tilbudgivern/backend/frontend/build/visual-reports-dashboard.html
T
alexpolo1 8773d96e94 feat: Add AI-Powered Visual Testing Suite using Selenium and OpenAI Vision API
- Implemented VisualAITester class for automated UI/UX testing
- Configured environment variables for API keys and paths
- Developed methods for browser setup, login, screenshot capture, and image encoding
- Integrated OpenAI Vision API for visual quality analysis with detailed feedback
- Created tests for various application pages including homepage, project flow, geometry form, and more
- Generated comprehensive HTML report summarizing test results and AI analysis
- Added functionality to copy AI prompts for automated issue resolution
2025-11-14 20:27:19 +00:00

394 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Visual AI Test Reports 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: 40px 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 40px;
}
.header h1 {
font-size: 3rem;
margin-bottom: 10px;
}
.header p {
font-size: 1.2rem;
opacity: 0.9;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.stat-card {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
text-align: center;
}
.stat-card .value {
font-size: 3rem;
font-weight: bold;
color: #667eea;
margin: 10px 0;
}
.stat-card .label {
color: #666;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.reports-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 25px;
}
.report-card {
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.report-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 48px rgba(0,0,0,0.15);
}
.report-card .card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
}
.report-card .card-header h3 {
font-size: 1.2rem;
margin-bottom: 5px;
}
.report-card .card-header .timestamp {
font-size: 0.9rem;
opacity: 0.9;
}
.report-card .card-body {
padding: 20px;
}
.report-card .info-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.report-card .info-row:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.report-card .info-label {
color: #666;
font-size: 0.9rem;
}
.report-card .info-value {
font-weight: 600;
color: #333;
}
.report-card .actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.loading {
text-align: center;
padding: 60px 20px;
color: white;
font-size: 1.5rem;
}
.loading .spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255,255,255,0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.empty-state h2 {
color: #333;
margin-bottom: 10px;
}
.empty-state p {
color: #666;
font-size: 1.1rem;
}
.latest-badge {
display: inline-block;
background: #28a745;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8rem;
margin-left: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 Visual AI Test Reports</h1>
<p>Automatisk UI/UX kvalitetsvurdering med AI</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="label">Total Rapporter</div>
<div class="value" id="totalReports">-</div>
</div>
<div class="stat-card">
<div class="label">Seneste Test</div>
<div class="value" id="latestTest" style="font-size: 1.5rem;">-</div>
</div>
<div class="stat-card">
<div class="label">Status</div>
<div class="value" id="status" style="font-size: 2rem;">⏳</div>
</div>
</div>
<div id="reportsContainer">
<div class="loading">
<div class="spinner"></div>
<p>Indlæser rapporter...</p>
</div>
</div>
</div>
<script>
// Format timestamp to readable date
function formatTimestamp(timestamp) {
// Format: YYYYMMDD_HHMMSS
const year = timestamp.substring(0, 4);
const month = timestamp.substring(4, 6);
const day = timestamp.substring(6, 8);
const hour = timestamp.substring(9, 11);
const minute = timestamp.substring(11, 13);
const second = timestamp.substring(13, 15);
return `${day}-${month}-${year} ${hour}:${minute}:${second}`;
}
// Format file size
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// Delete report
async function deleteReport(filename) {
if (!confirm('Er du sikker på at du vil slette denne rapport?')) {
return;
}
try {
const response = await fetch(`/api/visual-reports/${filename}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Rapport slettet!');
loadReports(); // Reload
} else {
alert('Fejl ved sletning af rapport');
}
} catch (error) {
alert('Fejl: ' + error.message);
}
}
// Load reports
async function loadReports() {
try {
const response = await fetch('/api/visual-reports/');
const data = await response.json();
if (!data.success) {
throw new Error(data.error);
}
// Update stats
document.getElementById('totalReports').textContent = data.count;
if (data.reports.length > 0) {
const latest = data.reports[0];
document.getElementById('latestTest').textContent = formatTimestamp(latest.timestamp);
document.getElementById('status').textContent = '✅';
} else {
document.getElementById('latestTest').textContent = 'Ingen';
document.getElementById('status').textContent = '❌';
}
// Render reports
const container = document.getElementById('reportsContainer');
if (data.reports.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h2>Ingen rapporter endnu</h2>
<p>Kør en visual AI test for at generere din første rapport</p>
<p style="margin-top: 20px; color: #999;">
<code>npm run test-visual-ai</code>
</p>
</div>
`;
return;
}
container.innerHTML = '<div class="reports-grid"></div>';
const grid = container.querySelector('.reports-grid');
data.reports.forEach((report, index) => {
const card = document.createElement('div');
card.className = 'report-card';
card.innerHTML = `
<div class="card-header">
<h3>
Test Rapport ${index === 0 ? '<span class="latest-badge">SENESTE</span>' : ''}
</h3>
<div class="timestamp">${formatTimestamp(report.timestamp)}</div>
</div>
<div class="card-body">
<div class="info-row">
<span class="info-label">📅 Dato</span>
<span class="info-value">${new Date(report.created).toLocaleDateString('da-DK')}</span>
</div>
<div class="info-row">
<span class="info-label">⏰ Tid</span>
<span class="info-value">${new Date(report.created).toLocaleTimeString('da-DK')}</span>
</div>
<div class="info-row">
<span class="info-label">📊 Størrelse</span>
<span class="info-value">${formatSize(report.size)}</span>
</div>
<div class="actions">
<a href="${report.url}" target="_blank" class="btn btn-primary">
📄 Åbn Rapport
</a>
<button onclick="deleteReport('${report.filename}')" class="btn btn-danger" title="Slet rapport">
🗑️
</button>
</div>
</div>
`;
grid.appendChild(card);
});
} catch (error) {
console.error('Error loading reports:', error);
document.getElementById('reportsContainer').innerHTML = `
<div class="empty-state">
<h2>❌ Fejl</h2>
<p>${error.message}</p>
</div>
`;
}
}
// Load on page load
loadReports();
// Auto-refresh every 30 seconds
setInterval(loadReports, 30000);
</script>
</body>
</html>