Changed "Success: 2" to "Processed 2 of 2" so the popup clearly reports processing status, not QC results. Processing errors only shown when they occur. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4303 lines
200 KiB
HTML
4303 lines
200 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>AI QC - Quality Control Platform</title>
|
||
<!-- Google Fonts - Montserrat -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<!-- MSAL Browser Library v2.35.0 (last working CDN version) -->
|
||
<script src="https://alcdn.msauth.net/browser/2.35.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||
<script>
|
||
// Check if MSAL library loaded successfully
|
||
if (typeof msal === 'undefined') {
|
||
console.error('MSAL library failed to load from CDN');
|
||
// Fallback to alternative CDN
|
||
document.write('<script src="https://cdn.jsdelivr.net/npm/@azure/msal-browser@2.35.0/lib/msal-browser.min.js"><\/script>');
|
||
}
|
||
</script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #1a1a1a 0%, #000000 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 20px;
|
||
padding: 30px;
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 40px;
|
||
border-bottom: 3px solid #FFC407;
|
||
padding-bottom: 20px;
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
.logo {
|
||
width: 120px;
|
||
height: auto;
|
||
}
|
||
|
||
.header-text p {
|
||
color: #7f8c8d;
|
||
font-size: 1.1em;
|
||
margin: 0;
|
||
margin-left: 0;
|
||
}
|
||
|
||
.settings-btn {
|
||
background: #f8f9fa;
|
||
border: 2px solid #dee2e6;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
transition: all 0.3s ease;
|
||
text-decoration: none;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.settings-btn:hover {
|
||
background: #e9ecef;
|
||
border-color: #adb5bd;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 30px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.form-section {
|
||
background: #f8f9fa;
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
border: 2px solid #e9ecef;
|
||
overflow: hidden;
|
||
min-width: 0;
|
||
}
|
||
|
||
.form-section h3 {
|
||
color: #495057;
|
||
margin-bottom: 20px;
|
||
font-size: 1.3em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
}
|
||
|
||
input[type="file"], select, textarea {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 2px solid #dee2e6;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
input[type="file"]:focus, select:focus, textarea:focus {
|
||
outline: none;
|
||
border-color: #FFC407;
|
||
}
|
||
|
||
.file-upload-area {
|
||
border: 2px dashed #cbd5e0;
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
background: #f7fafc;
|
||
}
|
||
|
||
.file-upload-area:hover {
|
||
border-color: #FFC407;
|
||
background: #edf2f7;
|
||
}
|
||
|
||
.file-upload-area.dragover {
|
||
border-color: #FFC407;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.file-info {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background: #d4edda;
|
||
border-radius: 8px;
|
||
display: none;
|
||
}
|
||
|
||
.btn {
|
||
padding: 15px 30px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 1.1em;
|
||
font-weight: 600;
|
||
transition: all 0.3s ease;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #FFC407 0%, #FFD700 100%);
|
||
color: #000000;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 10px 20px rgba(255, 196, 7, 0.5);
|
||
}
|
||
|
||
.btn-primary:disabled {
|
||
background: #6c757d;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.progress-container {
|
||
margin-top: 30px;
|
||
display: none;
|
||
background: #2c3e50;
|
||
border-radius: 15px;
|
||
padding: 40px;
|
||
color: white;
|
||
text-align: center;
|
||
}
|
||
|
||
.progress-header {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.progress-title {
|
||
font-size: 1.5em;
|
||
font-weight: 600;
|
||
margin-bottom: 15px;
|
||
color: #FFC407;
|
||
}
|
||
|
||
.progress-subtitle {
|
||
font-size: 1em;
|
||
color: #bdc3c7;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: #FFC407;
|
||
width: 0%;
|
||
transition: width 0.3s ease;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.current-app {
|
||
font-size: 1.1em;
|
||
color: #ecf0f1;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.processing-spinner {
|
||
display: inline-block;
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid rgba(255, 196, 7, 0.3);
|
||
border-top: 4px solid #FFC407;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 20px auto;
|
||
}
|
||
|
||
.results-container {
|
||
margin-top: 30px;
|
||
display: none;
|
||
}
|
||
|
||
.results-summary {
|
||
background: #f8f9fa;
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
border-left: 5px solid #28a745;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.results-summary h3 {
|
||
color: #495057;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.score-display {
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
color: #28a745;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.results-details {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
border: 2px solid #e9ecef;
|
||
}
|
||
|
||
.expandable-section {
|
||
margin-bottom: 15px;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.expandable-header {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
|
||
.expandable-header:hover {
|
||
background: #e9ecef;
|
||
}
|
||
|
||
.expandable-content {
|
||
padding: 20px;
|
||
display: none;
|
||
background: white;
|
||
}
|
||
|
||
.expandable-content.expanded {
|
||
display: block;
|
||
}
|
||
|
||
.check-score {
|
||
font-weight: bold;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
color: white;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.check-score.high { background: #28a745; }
|
||
.check-score.medium { background: #ffc107; color: #212529; }
|
||
.check-score.low { background: #dc3545; }
|
||
|
||
.pass-fail-badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-weight: bold;
|
||
font-size: 0.8em;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.pass-badge {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.fail-badge {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.overall-pass-fail {
|
||
font-size: 1.2em;
|
||
font-weight: bold;
|
||
margin-left: 15px;
|
||
padding: 8px 20px;
|
||
border-radius: 25px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.overall-pass {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.overall-fail {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.error-message {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.loading {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #FFC407;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.icon {
|
||
font-size: 1.2em;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.chevron {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.chevron.expanded {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
/* Client Selector Styles */
|
||
.client-card {
|
||
background: white;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 12px;
|
||
padding: 30px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.client-card:hover {
|
||
border-color: #1a73e8;
|
||
box-shadow: 0 4px 16px rgba(26, 115, 232, 0.2);
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.client-card.selected {
|
||
border-color: #1a73e8;
|
||
background: #e8f0fe;
|
||
box-shadow: 0 4px 16px rgba(26, 115, 232, 0.3);
|
||
}
|
||
|
||
.client-card h3 {
|
||
margin: 0 0 12px 0;
|
||
color: #1a73e8;
|
||
font-size: 1.8em;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.client-card p {
|
||
margin: 0;
|
||
color: #666;
|
||
font-size: 0.95em;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.client-card .profile-count {
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #e0e0e0;
|
||
color: #888;
|
||
font-size: 0.9em;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Main App Tabs */
|
||
.main-app-tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 25px;
|
||
border-bottom: 2px solid #dee2e6;
|
||
}
|
||
.main-tab-btn {
|
||
padding: 12px 28px;
|
||
border: none;
|
||
background: transparent;
|
||
color: #6c757d;
|
||
font-size: 1.05em;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
border-bottom: 3px solid transparent;
|
||
margin-bottom: -2px;
|
||
transition: all 0.2s;
|
||
font-family: 'Montserrat', sans-serif;
|
||
}
|
||
.main-tab-btn:hover {
|
||
color: #495057;
|
||
background: rgba(255, 196, 7, 0.08);
|
||
}
|
||
.main-tab-btn.active {
|
||
color: #1a1a1a;
|
||
border-bottom-color: #FFC407;
|
||
}
|
||
.main-tab-content {
|
||
display: none;
|
||
}
|
||
.main-tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
/* Client Reporting Dashboard */
|
||
.reporting-dashboard-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 15px;
|
||
margin-bottom: 25px;
|
||
}
|
||
.reporting-card {
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
}
|
||
.reporting-card .card-value {
|
||
font-size: 2em;
|
||
font-weight: 700;
|
||
margin-bottom: 4px;
|
||
}
|
||
.reporting-card .card-label {
|
||
font-size: 0.85em;
|
||
font-weight: 600;
|
||
}
|
||
.reporting-filters {
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: flex-end;
|
||
margin-bottom: 25px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.reporting-filters .filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
.reporting-filters label {
|
||
font-weight: 600;
|
||
color: #495057;
|
||
font-size: 0.9em;
|
||
}
|
||
.reporting-filters input[type="date"],
|
||
.reporting-filters select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 6px;
|
||
font-family: 'Montserrat', sans-serif;
|
||
font-size: 0.9em;
|
||
}
|
||
.reporting-load-btn {
|
||
padding: 8px 20px;
|
||
background: #1a73e8;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-family: 'Montserrat', sans-serif;
|
||
transition: background 0.2s;
|
||
}
|
||
.reporting-load-btn:hover {
|
||
background: #1557b0;
|
||
}
|
||
|
||
/* Admin Modal */
|
||
.admin-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1100;
|
||
}
|
||
.admin-modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
width: 90%;
|
||
max-width: 950px;
|
||
max-height: 85vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
}
|
||
.admin-modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 25px;
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
.admin-modal-header h2 {
|
||
margin: 0;
|
||
color: #333;
|
||
}
|
||
.admin-summary-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 15px;
|
||
padding: 20px 25px;
|
||
}
|
||
.admin-user-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.9em;
|
||
}
|
||
.admin-user-table thead tr {
|
||
background: #f8f9fa;
|
||
}
|
||
.admin-user-table th,
|
||
.admin-user-table td {
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
.admin-user-table th {
|
||
font-weight: 600;
|
||
color: #495057;
|
||
border-bottom: 2px solid #dee2e6;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<div class="header-content">
|
||
<svg class="logo" viewBox="0 0 278.84 109.39" xmlns="http://www.w3.org/2000/svg">
|
||
<defs>
|
||
<style>
|
||
.cls-1 { fill: #FFC407; }
|
||
.cls-2 { fill: #7f5800; }
|
||
.cls-3 { fill: #1a1a1a; }
|
||
</style>
|
||
</defs>
|
||
<g>
|
||
<path class="cls-3" d="M202.64,54.3c0-23.98,18.41-41.14,43.19-41.14,13.75,0,25.23,5,32.73,14.09l-11.82,10.91c-5.34-6.14-12.05-9.32-20-9.32-14.89,0-25.46,10.46-25.46,25.46s10.57,25.46,25.46,25.46c7.96,0,15.95-6.32,21.29-12.57l10.82,9.91c-7.5,9.21-19.27,18.35-33.13,18.35-24.66,0-43.07-17.16-43.07-41.14Z"/>
|
||
<g>
|
||
<path class="cls-3" d="M194.45,34.24s-.05-.1-.08-.15l-14.2,14.61c3.03,11.48-2.02,22.54-12.8,28.07-12.64,6.49-26.86,2.18-33.77-11.27-6.91-13.44-2.13-27.52,10.5-34.02,10.08-5.18,21.18-3.47,28.76,4.26l11.78-14.09c-12.52-10.82-30.75-12.84-47.72-4.12-22.34,11.48-30.71,35.46-19.91,56.49,9.87,19.21,32.96,26.13,53.61,18.58,6.83,7.12,8.31,8.77,13.83,12.3,7.58,4.86,20.83,4.94,27.21,4.03-13.71-10.35-21.39-21.99-24.73-27.15,12.57-12.59,16.02-30.98,7.5-47.56Z"/>
|
||
<path class="cls-1" d="M172.87,35.75l-11.76,14.06-15.86-8.46-7.5,8.79,23.83,17.69,18.6-19.13c-.52-1.98-1.28-3.97-2.3-5.94-1.39-2.71-3.09-5.05-5.01-7Z"/>
|
||
<path class="cls-1" d="M194.37,34.09l24.26-24.96-15.86-9.13-18.12,21.66c3.85,3.33,7.16,7.48,9.72,12.43Z"/>
|
||
<path class="cls-2" d="M177.88,42.76c1.02,1.98,1.77,3.97,2.3,5.94l14.2-14.61c-2.56-4.94-5.87-9.1-9.72-12.43l-11.78,14.09c1.92,1.96,3.61,4.29,5.01,7Z"/>
|
||
</g>
|
||
<g>
|
||
<path class="cls-3" d="M62.85,76.6H25.91l-7.05,17.05H0L35.46,14.09l17.18-1,36.57,80.55h-19.32l-7.05-17.05ZM57.05,62.62l-12.62-30.46-12.61,30.46h25.23Z"/>
|
||
<path class="cls-3" d="M89.55,13.93l18.41.52v79.55l-18.41-.36V13.93Z"/>
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
<div class="header-text">
|
||
<p>Quality Control Platform</p>
|
||
</div>
|
||
</div>
|
||
<div id="authSection">
|
||
<!-- Authentication loading state -->
|
||
<div id="authLoading" style="display: none;">
|
||
<div class="settings-btn">
|
||
<span>🔄</span>
|
||
Checking authentication...
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Login button -->
|
||
<button class="settings-btn" id="loginBtn" style="display: none;">
|
||
<span>🔐</span>
|
||
Sign In with Microsoft
|
||
</button>
|
||
|
||
<!-- User info when authenticated -->
|
||
<div id="userInfo" style="display: none;">
|
||
<div style="display: flex; align-items: center; gap: 15px;">
|
||
<div style="text-align: right;">
|
||
<div style="font-weight: 600; color: #495057;" id="userName"></div>
|
||
<div style="font-size: 0.9em; color: #6c757d;" id="userEmail"></div>
|
||
</div>
|
||
<!-- Client info display -->
|
||
<div id="clientInfo" style="display: none; margin-right: 5px; padding: 8px 12px; background: #e8f0fe; border-radius: 6px; border: 1px solid #1a73e8;">
|
||
<div style="font-size: 0.85em; color: #666; margin-bottom: 2px;">Client:</div>
|
||
<div style="font-weight: 600; color: #1a73e8; font-size: 0.95em;" id="currentClientName"></div>
|
||
<button id="switchClientBtn" style="
|
||
margin-top: 6px;
|
||
padding: 4px 10px;
|
||
background: white;
|
||
color: #1a73e8;
|
||
border: 1px solid #1a73e8;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.85em;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
" onmouseover="this.style.background='#1a73e8'; this.style.color='white';"
|
||
onmouseout="this.style.background='white'; this.style.color='#1a73e8';">
|
||
Switch Client
|
||
</button>
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button class="settings-btn" id="adminBtn" style="display: none; background: #6f42c1; color: white; border-color: #6f42c1;">
|
||
<span>👑</span>
|
||
Admin
|
||
</button>
|
||
<button class="settings-btn" id="settingsBtn">
|
||
<span>⚙️</span>
|
||
Settings
|
||
</button>
|
||
<button class="settings-btn" id="logoutBtn" style="background: #dc3545; color: white; border-color: #dc3545;">
|
||
<span>🚪</span>
|
||
Sign Out
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main application content - hidden until authenticated -->
|
||
<div id="mainApp" style="display: none;">
|
||
<!-- Main App Tab Navigation -->
|
||
<div class="main-app-tabs" id="mainAppTabs">
|
||
<button class="main-tab-btn active" onclick="showMainTab('analysis')">Analysis</button>
|
||
<button class="main-tab-btn" onclick="showMainTab('clientReporting')">Reporting</button>
|
||
</div>
|
||
|
||
<div id="analysisTab" class="main-tab-content active">
|
||
<div class="form-grid">
|
||
<div class="form-section">
|
||
<h3><span class="icon">📁</span>File Upload</h3>
|
||
<div class="form-group">
|
||
<label for="file-input">Select or drag your visual assets</label>
|
||
<div class="file-upload-area" id="fileUploadArea">
|
||
<input type="file" id="file-input" accept="image/*,.pdf" multiple style="display: none;">
|
||
<div style="font-size: 3em; margin-bottom: 10px;">📤</div>
|
||
<p>Click here or drag and drop your files</p>
|
||
<p style="font-size: 0.9em; color: #6c757d; margin-top: 5px;">
|
||
Supported formats: JPG, PNG, PDF, GIF, WebP<br>
|
||
Multiple files supported
|
||
</p>
|
||
</div>
|
||
<div class="file-info" id="fileInfo"></div>
|
||
|
||
<!-- Queue Display -->
|
||
<div id="fileQueue" style="margin-top: 20px; display: none;">
|
||
<h4 style="color: #495057; margin-bottom: 15px;">File Queue (<span id="queueCount">0</span> files)</h4>
|
||
<div id="queueList" style="max-height: 300px; overflow-y: auto; overflow-x: hidden;">
|
||
<!-- Queued files will appear here -->
|
||
</div>
|
||
<div style="margin-top: 15px; text-align: center;">
|
||
<button onclick="clearQueue()" style="background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; margin-right: 10px;">Clear All</button>
|
||
<button onclick="processQueue()" id="processQueueBtn" style="background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer;" disabled>Process Queue</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-section">
|
||
<h3><span class="icon">⚙️</span>QC Configuration</h3>
|
||
<div class="form-group">
|
||
<label for="profile-select">QC Profile</label>
|
||
<select id="profile-select">
|
||
<option value="">Loading profiles...</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<div class="cost-display" id="costDisplay" style="background: #e3f2fd; padding: 12px; border-radius: 8px; border-left: 4px solid #2196f3; display: none;">
|
||
<strong>Estimated Cost: <span id="estimatedCost">$0.00</span></strong>
|
||
<br>
|
||
<small style="color: #666;">Based on <span id="checkCount">0</span> selected checks at ~$0.01 each</small>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="output-mode">Output Mode</label>
|
||
<select id="output-mode">
|
||
<option value="html">HTML Report</option>
|
||
<option value="json">JSON Data</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="model-select">AI Model</label>
|
||
<select id="model-select">
|
||
<option value="">Use Profile Default</option>
|
||
<!-- Options will be populated dynamically from API -->
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="reference-asset-select">Reference Assets</label>
|
||
<select id="reference-asset-select">
|
||
<option value="">No reference asset selected</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="media-plan-select">Media Plan</label>
|
||
<select id="media-plan-select">
|
||
<option value="">No media plan selected</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div style="text-align: center; margin-bottom: 30px;">
|
||
<button class="btn btn-primary" id="analyzeBtn" disabled>
|
||
<span class="icon">🔍</span>
|
||
Start QC Analysis
|
||
</button>
|
||
</div>
|
||
|
||
<div class="progress-container" id="progressContainer">
|
||
<div class="progress-header">
|
||
<div class="progress-title" id="progressTitle">PROCESSING</div>
|
||
<div class="progress-subtitle" id="progressSubtitle">Loading appropriate profile resources...</div>
|
||
<div class="processing-spinner"></div>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progressFill"></div>
|
||
</div>
|
||
<div class="current-app" id="currentApp">Initializing analysis...</div>
|
||
</div>
|
||
|
||
<div class="results-container" id="resultsContainer">
|
||
<div class="results-summary">
|
||
<h3>Analysis Results</h3>
|
||
<div class="score-display" id="overallScore">--/100</div>
|
||
<div id="gradeDisplay">Grade: --</div>
|
||
<div id="profileUsed">Profile: --</div>
|
||
<div id="checksCompleted">Checks: --</div>
|
||
</div>
|
||
|
||
<div class="results-details" id="resultsDetails">
|
||
<!-- Results will be populated here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="saved-files-container" id="savedFilesContainer" style="display: none;">
|
||
<div style="background: #f8f9fa; padding: 25px; border-radius: 15px; border-left: 5px solid #6c757d; margin-bottom: 20px;">
|
||
<h3>📁 Saved Output Files</h3>
|
||
<div id="savedFilesList">
|
||
<!-- Saved files will be populated here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div> <!-- End analysisTab -->
|
||
|
||
<!-- Client Reporting Dashboard Tab -->
|
||
<div id="clientReportingTab" class="main-tab-content">
|
||
<div style="margin-bottom: 20px;">
|
||
<h2 style="color: #333; margin-bottom: 8px;" id="reportingClientTitle">Usage Report</h2>
|
||
<p style="color: #6c757d; font-size: 0.95em;">View usage statistics and analysis history for this client.</p>
|
||
</div>
|
||
|
||
<div class="reporting-filters">
|
||
<div class="filter-group">
|
||
<label>Start Date</label>
|
||
<input type="date" id="reportingStartDate">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>End Date</label>
|
||
<input type="date" id="reportingEndDate">
|
||
</div>
|
||
<button class="reporting-load-btn" onclick="loadClientReportingData()">Load Report</button>
|
||
</div>
|
||
|
||
<div class="reporting-dashboard-cards" id="clientReportingCards" style="display: none;">
|
||
<div class="reporting-card" style="background: #e3f2fd;">
|
||
<div class="card-value" style="color: #1565c0;" id="crTotalAnalyses">-</div>
|
||
<div class="card-label" style="color: #1565c0;">Total Analyses</div>
|
||
</div>
|
||
<div class="reporting-card" style="background: #e8f5e9;">
|
||
<div class="card-value" style="color: #2e7d32;" id="crUniqueUsers">-</div>
|
||
<div class="card-label" style="color: #2e7d32;">Unique Users</div>
|
||
</div>
|
||
<div class="reporting-card" style="background: #fff3e0;">
|
||
<div class="card-value" style="color: #e65100;" id="crTotalChecks">-</div>
|
||
<div class="card-label" style="color: #e65100;">Total Checks Run</div>
|
||
</div>
|
||
<div class="reporting-card" style="background: #fce4ec;">
|
||
<div class="card-value" style="color: #c62828;" id="crEstCost">-</div>
|
||
<div class="card-label" style="color: #c62828;">Estimated Cost</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="clientReportingTable" style="display: none; max-height: 400px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 10px;">
|
||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
|
||
<thead>
|
||
<tr style="background: #f8f9fa; position: sticky; top: 0;">
|
||
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #dee2e6;">Date</th>
|
||
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #dee2e6;">User</th>
|
||
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #dee2e6;">Profile</th>
|
||
<th style="padding: 12px; text-align: center; border-bottom: 2px solid #dee2e6;">Checks</th>
|
||
<th style="padding: 12px; text-align: center; border-bottom: 2px solid #dee2e6;">Score</th>
|
||
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #dee2e6;">Cost</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="clientReportingTableBody"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="clientReportingEmpty" style="text-align: center; color: #6c757d; padding: 50px;">
|
||
<div style="font-size: 2.5em; margin-bottom: 15px;">📊</div>
|
||
<p style="font-size: 1.1em;">Click "Load Report" to view usage statistics</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div> <!-- End mainApp -->
|
||
|
||
<!-- Authentication required screen -->
|
||
<div id="authRequired" style="display: block;">
|
||
<div style="text-align: center; padding: 60px 20px;">
|
||
<div style="font-size: 4em; margin-bottom: 20px;">🔐</div>
|
||
<h2 style="color: #495057; margin-bottom: 15px;">Authentication Required</h2>
|
||
<p style="color: #6c757d; font-size: 1.1em; margin-bottom: 30px;">
|
||
Please sign in with your Microsoft account to access the AI QC Platform
|
||
</p>
|
||
<button class="settings-btn" id="authRequiredLoginBtn" style="background: #0078d4; color: white; border-color: #0078d4; font-size: 1.1em; padding: 15px 30px;">
|
||
<span>🔐</span>
|
||
Sign In with Microsoft
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Client Selection Screen -->
|
||
<div id="clientSelector" style="display: none;">
|
||
<div class="container" style="max-width: 900px; margin-top: 80px;">
|
||
<div class="header" style="margin-bottom: 30px; text-align: center;">
|
||
<h1 style="color: #333; font-size: 2.5em; margin-bottom: 10px;">Select Your Client</h1>
|
||
<p style="color: #666; margin-top: 10px; font-size: 1.1em;">Choose the client you're working with to see relevant QC profiles</p>
|
||
</div>
|
||
|
||
<div class="client-grid" id="clientGrid" style="
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 30px;
|
||
">
|
||
<!-- Client cards will be populated here -->
|
||
</div>
|
||
|
||
<div style="text-align: center; margin-top: 40px;">
|
||
<button id="logoutFromClientSelector" class="settings-btn" style="
|
||
background: #dc3545;
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 1em;
|
||
">Logout</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Section (full page, admin users only) -->
|
||
<div id="adminSection" style="display: none;">
|
||
<div style="max-width: 1000px; margin: 0 auto;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;">
|
||
<div>
|
||
<h2 style="color: #333; margin-bottom: 5px;">Administration Panel</h2>
|
||
<p style="color: #6c757d;">Platform user activity overview</p>
|
||
</div>
|
||
<button onclick="hideAdminSection()" class="settings-btn" style="background: #6c757d; color: white; border-color: #6c757d;">
|
||
Back to App
|
||
</button>
|
||
</div>
|
||
|
||
<div class="admin-summary-cards" id="adminSummaryCards" style="padding: 0; margin-bottom: 25px;">
|
||
<div class="reporting-card" style="background: #e3f2fd;">
|
||
<div class="card-value" style="color: #1565c0;" id="adminTotalUsers">-</div>
|
||
<div class="card-label" style="color: #1565c0;">Total Users</div>
|
||
</div>
|
||
<div class="reporting-card" style="background: #e8f5e9;">
|
||
<div class="card-value" style="color: #2e7d32;" id="adminTotalAnalyses">-</div>
|
||
<div class="card-label" style="color: #2e7d32;">Total Platform Analyses</div>
|
||
</div>
|
||
<div class="reporting-card" style="background: #fce4ec;">
|
||
<div class="card-value" style="color: #c62828;" id="adminTotalCost">-</div>
|
||
<div class="card-label" style="color: #c62828;">Total Estimated Cost</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="color: #495057; margin-bottom: 15px;">Platform Users</h3>
|
||
<div id="adminUserTableContainer" style="max-height: 500px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 10px;">
|
||
<table class="admin-user-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Email</th>
|
||
<th style="text-align: center;">Analyses</th>
|
||
<th style="text-align: center;">Total Checks</th>
|
||
<th>Clients Used</th>
|
||
<th>Last Active</th>
|
||
<th style="text-align: right;">Est. Cost</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="adminUserTableBody">
|
||
<tr><td colspan="7" style="text-align: center; padding: 30px; color: #6c757d;">Loading users...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Get the base path for API calls
|
||
function getBasePath() {
|
||
const path = window.location.pathname;
|
||
// If we're at /ai_qc or /ai_qc/ or /ai_qc/something, extract the base path
|
||
if (path.includes('/ai_qc')) {
|
||
// Handle both /ai_qc and /ai_qc/ cases
|
||
const index = path.indexOf('/ai_qc');
|
||
return path.substring(0, index) + '/ai_qc/'; // Always return with trailing slash
|
||
}
|
||
return '/'; // Default to root if no special path
|
||
}
|
||
|
||
const BASE_PATH = getBasePath();
|
||
console.log('Base path detected:', BASE_PATH);
|
||
|
||
// Global variables
|
||
let selectedFile = null;
|
||
let availableProfiles = {};
|
||
let currentSessionId = null;
|
||
let fileQueue = [];
|
||
let selectedClient = null;
|
||
let availableClients = {};
|
||
let selectedModel = null;
|
||
let availableModels = {};
|
||
let isAdmin = false;
|
||
|
||
// DOM elements (will be set in init function when DOM is ready)
|
||
let fileUploadArea, fileInput, fileInfo, profileSelect, outputMode, modelSelect, analyzeBtn, progressContainer, progressFill, currentApp, resultsContainer;
|
||
|
||
// Initialize the application
|
||
async function init() {
|
||
console.log('=== INIT FUNCTION STARTED ===');
|
||
|
||
// Initialize DOM elements now that DOM is ready
|
||
fileUploadArea = document.getElementById('fileUploadArea');
|
||
fileInput = document.getElementById('file-input');
|
||
fileInfo = document.getElementById('fileInfo');
|
||
profileSelect = document.getElementById('profile-select');
|
||
outputMode = document.getElementById('output-mode');
|
||
modelSelect = document.getElementById('model-select');
|
||
analyzeBtn = document.getElementById('analyzeBtn');
|
||
progressContainer = document.getElementById('progressContainer');
|
||
progressFill = document.getElementById('progressFill');
|
||
currentApp = document.getElementById('currentApp');
|
||
resultsContainer = document.getElementById('resultsContainer');
|
||
|
||
console.log('DOM elements initialized:', {
|
||
profileSelect: profileSelect,
|
||
fileInput: fileInput,
|
||
analyzeBtn: analyzeBtn
|
||
});
|
||
|
||
try {
|
||
console.log('About to load profiles...');
|
||
console.log('selectedClient in init():', selectedClient);
|
||
await loadProfiles(selectedClient);
|
||
console.log('Profiles loaded successfully');
|
||
|
||
console.log('About to load saved files...');
|
||
console.log('selectedClient before loadSavedFiles():', selectedClient);
|
||
await loadSavedFiles();
|
||
console.log('Saved files loaded successfully');
|
||
|
||
console.log('About to setup event listeners...');
|
||
setupEventListeners();
|
||
console.log('Event listeners setup complete');
|
||
} catch (error) {
|
||
console.error('Error in init function:', error);
|
||
}
|
||
console.log('=== INIT FUNCTION COMPLETE ===');
|
||
}
|
||
|
||
// Load available profiles from API
|
||
async function loadProfiles(clientId = null) {
|
||
try {
|
||
console.log('Loading profiles from API...');
|
||
console.log('Client ID:', clientId);
|
||
console.log('Current URL:', window.location.href);
|
||
|
||
// Build URL with optional client filter
|
||
let url = `${BASE_PATH}api/profiles`;
|
||
if (clientId) {
|
||
url += `?client=${clientId}`;
|
||
}
|
||
console.log(`Attempting to fetch: ${url}`);
|
||
|
||
const response = await fetch(url, {
|
||
credentials: 'include'
|
||
});
|
||
console.log('Response received:', response);
|
||
console.log('Response status:', response.status);
|
||
console.log('Response ok:', response.ok);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('Profiles API response:', data);
|
||
console.log('Response data.status:', data.status);
|
||
console.log('Status check result:', data.status === 'success');
|
||
|
||
if (data.status === 'success') {
|
||
availableProfiles = data.all_profiles;
|
||
console.log('Available profiles:', availableProfiles);
|
||
console.log('About to call populateProfileSelect()...');
|
||
console.log('populateProfileSelect function exists:', typeof populateProfileSelect);
|
||
try {
|
||
populateProfileSelect();
|
||
console.log('populateProfileSelect() called successfully');
|
||
} catch (error) {
|
||
console.error('Error calling populateProfileSelect():', error);
|
||
}
|
||
|
||
// Direct dropdown update as backup
|
||
console.log('Attempting direct dropdown update...');
|
||
const dropdown = document.getElementById('profile-select');
|
||
if (dropdown) {
|
||
console.log('Found dropdown element directly, updating...');
|
||
dropdown.innerHTML = '<option value="">Select a profile...</option>';
|
||
for (const [profileId, profileInfo] of Object.entries(availableProfiles)) {
|
||
const option = document.createElement('option');
|
||
option.value = profileId;
|
||
option.textContent = `${profileInfo.name} (${profileInfo.enabled_count} checks)`;
|
||
dropdown.appendChild(option);
|
||
}
|
||
console.log('Direct dropdown update complete with', dropdown.children.length - 1, 'profiles');
|
||
} else {
|
||
console.error('Could not find dropdown element for direct update');
|
||
}
|
||
} else {
|
||
console.error('Failed to load profiles, data.status:', data.status);
|
||
console.error('Full response data:', data);
|
||
// Show error in dropdown
|
||
profileSelect.innerHTML = '<option value="">Error loading profiles</option>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading profiles:', error);
|
||
console.error('Error details:', error.message, error.stack);
|
||
|
||
// Try a direct test fetch
|
||
console.log('Attempting direct health check...');
|
||
try {
|
||
const healthResponse = await fetch(`${BASE_PATH}health`);
|
||
console.log('Health check response:', healthResponse.status);
|
||
} catch (healthError) {
|
||
console.error('Health check failed:', healthError);
|
||
}
|
||
|
||
// Show error in dropdown with more details
|
||
profileSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
|
||
|
||
// Add a manual refresh option
|
||
const refreshOption = document.createElement('option');
|
||
refreshOption.value = 'refresh';
|
||
refreshOption.textContent = 'Click to retry loading profiles';
|
||
profileSelect.appendChild(refreshOption);
|
||
|
||
// Add event listener for manual retry
|
||
profileSelect.addEventListener('change', function(e) {
|
||
if (e.target.value === 'refresh') {
|
||
loadProfiles(selectedClient);
|
||
}
|
||
}, { once: true });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load available clients from the API
|
||
*/
|
||
async function loadClients() {
|
||
try {
|
||
console.log('Loading clients from API...');
|
||
const response = await fetch(`${BASE_PATH}api/clients`, {
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('Clients API response:', data);
|
||
|
||
if (data.status === 'success') {
|
||
availableClients = data.clients;
|
||
populateClientSelector();
|
||
} else {
|
||
throw new Error(data.message || 'Failed to load clients');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading clients:', error);
|
||
alert('Failed to load clients. Please refresh the page.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Populate the client selector with available clients
|
||
*/
|
||
function populateClientSelector() {
|
||
const clientGrid = document.getElementById('clientGrid');
|
||
if (!clientGrid) {
|
||
console.error('Client grid element not found');
|
||
return;
|
||
}
|
||
|
||
clientGrid.innerHTML = '';
|
||
|
||
for (const [clientId, clientInfo] of Object.entries(availableClients)) {
|
||
const clientCard = document.createElement('div');
|
||
clientCard.className = 'client-card';
|
||
clientCard.dataset.clientId = clientId;
|
||
|
||
const profileCount = clientInfo.profiles.length;
|
||
const profileText = profileCount === 1 ? 'profile' : 'profiles';
|
||
|
||
clientCard.innerHTML = `
|
||
<h3>${clientInfo.display_name}</h3>
|
||
<p>${clientInfo.description}</p>
|
||
<div class="profile-count">${profileCount} ${profileText} available</div>
|
||
`;
|
||
|
||
clientCard.addEventListener('click', () => selectClient(clientId));
|
||
clientGrid.appendChild(clientCard);
|
||
}
|
||
|
||
console.log('Client selector populated with', Object.keys(availableClients).length, 'clients');
|
||
}
|
||
|
||
/**
|
||
* Handle client selection
|
||
*/
|
||
async function selectClient(clientId) {
|
||
try {
|
||
console.log('Selecting client:', clientId);
|
||
selectedClient = clientId;
|
||
|
||
// Save to localStorage for persistence
|
||
localStorage.setItem('selectedClient', clientId);
|
||
|
||
// Visual feedback
|
||
document.querySelectorAll('.client-card').forEach(card => {
|
||
card.classList.remove('selected');
|
||
});
|
||
const selectedCard = document.querySelector(`[data-client-id="${clientId}"]`);
|
||
if (selectedCard) {
|
||
selectedCard.classList.add('selected');
|
||
}
|
||
|
||
// Load profiles for this client
|
||
await loadProfiles(clientId);
|
||
|
||
// Small delay for better UX
|
||
await new Promise(resolve => setTimeout(resolve, 300));
|
||
|
||
// Hide client selector and show main app
|
||
document.getElementById('clientSelector').style.display = 'none';
|
||
document.getElementById('mainApp').style.display = 'block';
|
||
|
||
// Initialize the app
|
||
init();
|
||
updateReferenceAssetsDropdown();
|
||
updateMediaPlanDropdown();
|
||
|
||
// Clear saved files from previous client immediately
|
||
const savedFilesList = document.getElementById('savedFilesList');
|
||
const savedFilesContainer = document.getElementById('savedFilesContainer');
|
||
if (savedFilesList) savedFilesList.innerHTML = '';
|
||
if (savedFilesContainer) savedFilesContainer.style.display = 'none';
|
||
|
||
// Reset reporting dashboard for new client
|
||
resetClientReporting();
|
||
|
||
// Switch to Analysis tab
|
||
showMainTab('analysis');
|
||
|
||
// Update auth UI to show client info
|
||
updateAuthUI();
|
||
|
||
console.log('Client selection complete');
|
||
|
||
} catch (error) {
|
||
console.error('Error selecting client:', error);
|
||
alert('Failed to load profiles for selected client. Please try again.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show client selector screen
|
||
*/
|
||
function showClientSelector() {
|
||
console.log('Showing client selector');
|
||
document.getElementById('authRequired').style.display = 'none';
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('adminSection').style.display = 'none';
|
||
document.getElementById('clientSelector').style.display = 'block';
|
||
|
||
loadClients();
|
||
}
|
||
|
||
/**
|
||
* Setup client switch button handler
|
||
*/
|
||
function setupClientSwitchHandler() {
|
||
const switchBtn = document.getElementById('switchClientBtn');
|
||
if (switchBtn) {
|
||
switchBtn.addEventListener('click', function() {
|
||
console.log('Switch client button clicked');
|
||
// Clear current selection
|
||
selectedClient = null;
|
||
localStorage.removeItem('selectedClient');
|
||
|
||
// Show client selector
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('clientInfo').style.display = 'none';
|
||
showClientSelector();
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load available models from API
|
||
*/
|
||
async function loadModels() {
|
||
try {
|
||
console.log('Loading models from API...');
|
||
const response = await fetch(`${BASE_PATH}api/models`, {
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('Models API response:', data);
|
||
|
||
if (data.status === 'success') {
|
||
availableModels = data.models;
|
||
populateModelSelect();
|
||
|
||
// Load saved preference
|
||
const savedModel = localStorage.getItem('selectedModel');
|
||
if (savedModel && availableModels[savedModel]) {
|
||
selectModel(savedModel);
|
||
}
|
||
} else {
|
||
throw new Error(data.message || 'Failed to load models');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading models:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Populate model selection dropdown
|
||
*/
|
||
function populateModelSelect() {
|
||
// Populate both the main UI model selector and the Settings modal selector
|
||
const mainUISelect = document.getElementById('model-select');
|
||
const settingsSelect = document.getElementById('modelSelect');
|
||
|
||
// Populate main UI selector
|
||
if (mainUISelect) {
|
||
mainUISelect.innerHTML = '<option value="">Use Profile Default</option>';
|
||
|
||
for (const [modelId, modelInfo] of Object.entries(availableModels)) {
|
||
const option = document.createElement('option');
|
||
option.value = modelId;
|
||
|
||
let displayName = modelInfo.name;
|
||
if (modelInfo.status === 'beta') {
|
||
displayName += ' [BETA]';
|
||
}
|
||
|
||
option.textContent = displayName;
|
||
option.title = modelInfo.description;
|
||
|
||
mainUISelect.appendChild(option);
|
||
}
|
||
|
||
// Sync with selected model
|
||
if (selectedModel) {
|
||
mainUISelect.value = selectedModel;
|
||
}
|
||
|
||
// Add change listener to update selectedModel
|
||
mainUISelect.addEventListener('change', function() {
|
||
const modelId = this.value;
|
||
selectModel(modelId);
|
||
});
|
||
|
||
console.log('Main UI model select populated with', Object.keys(availableModels).length, 'models');
|
||
}
|
||
|
||
// Populate Settings modal selector
|
||
if (settingsSelect) {
|
||
settingsSelect.innerHTML = '<option value="">Use Profile Default</option>';
|
||
|
||
for (const [modelId, modelInfo] of Object.entries(availableModels)) {
|
||
const option = document.createElement('option');
|
||
option.value = modelId;
|
||
|
||
let displayName = modelInfo.name;
|
||
if (modelInfo.status === 'beta') {
|
||
displayName += ' [BETA]';
|
||
}
|
||
|
||
option.textContent = displayName;
|
||
option.title = modelInfo.description;
|
||
|
||
settingsSelect.appendChild(option);
|
||
}
|
||
|
||
// Add change listener for beta warning
|
||
settingsSelect.addEventListener('change', function() {
|
||
const selectedModelId = this.value;
|
||
updateBetaWarning(selectedModelId);
|
||
});
|
||
|
||
// Set current value if model is selected
|
||
if (selectedModel) {
|
||
settingsSelect.value = selectedModel;
|
||
updateBetaWarning(selectedModel);
|
||
}
|
||
|
||
console.log('Settings model select populated');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update beta warning display
|
||
*/
|
||
function updateBetaWarning(modelId) {
|
||
const betaWarning = document.getElementById('betaWarning');
|
||
const currentDisplay = document.getElementById('currentModelDisplay');
|
||
|
||
if (!betaWarning || !currentDisplay) return;
|
||
|
||
if (modelId && availableModels[modelId]) {
|
||
const modelInfo = availableModels[modelId];
|
||
|
||
if (modelInfo.status === 'beta') {
|
||
betaWarning.style.display = 'block';
|
||
} else {
|
||
betaWarning.style.display = 'none';
|
||
}
|
||
|
||
currentDisplay.textContent = modelInfo.name;
|
||
if (modelInfo.status === 'beta') {
|
||
currentDisplay.innerHTML = modelInfo.name + ' <span style="color: #ff9800; font-weight: bold;">[BETA]</span>';
|
||
}
|
||
} else {
|
||
betaWarning.style.display = 'none';
|
||
currentDisplay.textContent = 'Use Profile Default';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Select and save model preference
|
||
*/
|
||
function selectModel(modelId) {
|
||
selectedModel = modelId;
|
||
localStorage.setItem('selectedModel', modelId);
|
||
|
||
// Update both model selectors to keep them in sync
|
||
const mainUISelect = document.getElementById('model-select');
|
||
const settingsSelect = document.getElementById('modelSelect');
|
||
|
||
if (mainUISelect) {
|
||
mainUISelect.value = modelId;
|
||
}
|
||
|
||
if (settingsSelect) {
|
||
settingsSelect.value = modelId;
|
||
updateBetaWarning(modelId);
|
||
}
|
||
|
||
console.log(`Model selected: ${modelId || 'Profile Default'}`);
|
||
}
|
||
|
||
/**
|
||
* Save model selection (called from settings modal button)
|
||
*/
|
||
function saveModelSelection() {
|
||
const modelSelect = document.getElementById('modelSelect');
|
||
if (!modelSelect) return;
|
||
|
||
const selectedModelId = modelSelect.value;
|
||
selectModel(selectedModelId);
|
||
|
||
alert(selectedModelId
|
||
? `Model preference saved: ${availableModels[selectedModelId].name}`
|
||
: 'Model preference cleared. Will use profile default.'
|
||
);
|
||
}
|
||
|
||
|
||
// Load saved output files from API with retry mechanism
|
||
async function loadSavedFiles(retryAttempts = 0, maxRetries = 0) {
|
||
// Capture client at call time to prevent stale closures
|
||
const clientForThisLoad = selectedClient;
|
||
|
||
try {
|
||
console.log(`Loading saved files for client '${clientForThisLoad}'...`);
|
||
|
||
// Build URL with client filter if selected
|
||
let url = `${BASE_PATH}api/output_files`;
|
||
if (clientForThisLoad) {
|
||
url += `?client=${clientForThisLoad}`;
|
||
}
|
||
|
||
const response = await fetch(url, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
// Abort if client changed while we were fetching
|
||
if (selectedClient !== clientForThisLoad) {
|
||
console.log('Client changed during fetch, discarding results');
|
||
return;
|
||
}
|
||
|
||
if (data.files && data.files.length > 0) {
|
||
console.log(`Found ${data.files.length} saved files`);
|
||
displaySavedFiles(data.files);
|
||
} else {
|
||
console.log('No saved files found');
|
||
// Clear the saved files list and hide the container
|
||
const savedFilesContainer = document.getElementById('savedFilesContainer');
|
||
const savedFilesList = document.getElementById('savedFilesList');
|
||
if (savedFilesList) savedFilesList.innerHTML = '';
|
||
if (savedFilesContainer) savedFilesContainer.style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading saved files:', error);
|
||
}
|
||
}
|
||
|
||
// Refresh saved files after analysis with progressive delays
|
||
async function refreshSavedFilesAfterAnalysis() {
|
||
console.log('Starting post-analysis file refresh...');
|
||
|
||
// Show loading indicator
|
||
showSavedFilesLoading(true);
|
||
|
||
// Store the current number of files for comparison
|
||
let previousFileCount = 0;
|
||
try {
|
||
// Build URL with client filter if selected
|
||
let url = `${BASE_PATH}api/output_files`;
|
||
if (selectedClient) {
|
||
url += `?client=${selectedClient}`;
|
||
}
|
||
|
||
const response = await fetch(url, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
previousFileCount = data.files ? data.files.length : 0;
|
||
console.log(`Current file count: ${previousFileCount}`);
|
||
} catch (error) {
|
||
console.log('Could not get initial file count, proceeding with refresh');
|
||
}
|
||
|
||
// Attempt refresh multiple times with increasing delays
|
||
const refreshAttempts = [1000, 3000, 5000]; // 1s, 3s, 5s delays
|
||
|
||
for (let i = 0; i < refreshAttempts.length; i++) {
|
||
await new Promise(resolve => setTimeout(resolve, refreshAttempts[i]));
|
||
|
||
try {
|
||
console.log(`Refresh attempt ${i + 1} after ${refreshAttempts[i]}ms delay`);
|
||
|
||
// Build URL with client filter if selected
|
||
let refreshUrl = `${BASE_PATH}api/output_files`;
|
||
if (selectedClient) {
|
||
refreshUrl += `?client=${selectedClient}`;
|
||
console.log(`Filtering refresh by client: ${selectedClient}`);
|
||
}
|
||
|
||
const response = await fetch(refreshUrl, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.files && data.files.length > 0) {
|
||
const newFileCount = data.files.length;
|
||
console.log(`Found ${newFileCount} files (was ${previousFileCount})`);
|
||
|
||
// Highlight latest file if new files were added
|
||
const shouldHighlight = newFileCount > previousFileCount;
|
||
displaySavedFiles(data.files, shouldHighlight);
|
||
|
||
// If we found new files, we can stop trying
|
||
if (newFileCount > previousFileCount) {
|
||
console.log('New file(s) detected, refresh complete');
|
||
break;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(`Refresh attempt ${i + 1} failed:`, error);
|
||
}
|
||
}
|
||
|
||
// Hide loading indicator
|
||
showSavedFilesLoading(false);
|
||
}
|
||
|
||
// Show/hide loading indicator for saved files section
|
||
function showSavedFilesLoading(show) {
|
||
const savedFilesContainer = document.getElementById('savedFilesContainer');
|
||
const savedFilesList = document.getElementById('savedFilesList');
|
||
|
||
if (show) {
|
||
// Show container if hidden
|
||
savedFilesContainer.style.display = 'block';
|
||
|
||
// Add loading indicator
|
||
const loadingDiv = document.createElement('div');
|
||
loadingDiv.id = 'savedFilesLoading';
|
||
loadingDiv.style.cssText = 'text-align: center; padding: 20px; color: #6c757d;';
|
||
loadingDiv.innerHTML = '🔄 Checking for new files...';
|
||
|
||
// Remove existing loading indicator if any
|
||
const existingLoading = document.getElementById('savedFilesLoading');
|
||
if (existingLoading) {
|
||
existingLoading.remove();
|
||
}
|
||
|
||
savedFilesList.appendChild(loadingDiv);
|
||
} else {
|
||
// Remove loading indicator
|
||
const loadingDiv = document.getElementById('savedFilesLoading');
|
||
if (loadingDiv) {
|
||
loadingDiv.remove();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Display saved files in the UI
|
||
function displaySavedFiles(files, highlightLatest = false) {
|
||
const savedFilesContainer = document.getElementById('savedFilesContainer');
|
||
const savedFilesList = document.getElementById('savedFilesList');
|
||
|
||
savedFilesList.innerHTML = '';
|
||
|
||
// Add consolidation controls bar at the top
|
||
let controlsBar = document.getElementById('consolidateControls');
|
||
if (!controlsBar) {
|
||
controlsBar = document.createElement('div');
|
||
controlsBar.id = 'consolidateControls';
|
||
controlsBar.style.cssText = 'display: none; background: #e3f2fd; border: 1px solid #90caf9; border-radius: 8px; padding: 10px 15px; margin-bottom: 10px; align-items: center; justify-content: space-between;';
|
||
savedFilesList.parentNode.insertBefore(controlsBar, savedFilesList);
|
||
}
|
||
controlsBar.innerHTML =
|
||
'<span id="consolidateCount" style="font-weight: 600; color: #1565c0; font-size: 0.9em;">0 reports selected</span>' +
|
||
'<div>' +
|
||
'<button onclick="clearFileSelection()" style="background: #6c757d; color: white; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 0.85em; margin-right: 8px;">Clear</button>' +
|
||
'<button onclick="deleteSelectedReports()" id="deleteSelectedBtn" style="background: #dc3545; color: white; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 0.85em; margin-right: 8px;">Delete Selected</button>' +
|
||
'<button onclick="consolidateSelectedReports()" id="consolidateBtn" style="background: #1565c0; color: white; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 0.85em;" disabled>Consolidate Reports</button>' +
|
||
'</div>';
|
||
controlsBar.style.display = 'none';
|
||
|
||
files.forEach((file, index) => {
|
||
const fileDiv = document.createElement('div');
|
||
|
||
// Highlight the latest file (first in sorted list) if requested
|
||
const isLatest = index === 0 && highlightLatest;
|
||
const baseStyle = 'background: white; padding: 15px; margin: 10px 0; border-radius: 8px; border: 1px solid #dee2e6; transition: all 0.3s ease;';
|
||
const highlightStyle = isLatest ? 'background: #e8f5e8; border: 2px solid #28a745; box-shadow: 0 4px 8px rgba(40, 167, 69, 0.2);' : '';
|
||
|
||
fileDiv.style.cssText = baseStyle + highlightStyle;
|
||
|
||
const fileType = file.filename.endsWith('.html') ? 'HTML Report' : 'JSON Data';
|
||
const fileIcon = file.filename.endsWith('.html') ? '🌐' : '📄';
|
||
const fileSize = (file.size / 1024).toFixed(1) + ' KB';
|
||
const newBadge = isLatest ? '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; margin-left: 8px;">NEW</span>' : '';
|
||
const isHtmlReport = file.filename.endsWith('.html');
|
||
const checkboxHtml = isHtmlReport ?
|
||
'<input type="checkbox" class="report-select-cb" data-filename="' + file.filename + '" data-client="' + (file.client || '') + '" onchange="updateConsolidateControls()" style="width: 18px; height: 18px; margin-right: 10px; cursor: pointer; flex-shrink: 0;">' : '';
|
||
|
||
fileDiv.innerHTML =
|
||
'<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px;">' +
|
||
'<div style="display: flex; align-items: center; min-width: 0; flex: 1;">' +
|
||
checkboxHtml +
|
||
'<div style="min-width: 0;">' +
|
||
'<strong style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">' + fileIcon + ' ' + file.filename + newBadge + '</strong>' +
|
||
'<small style="color: #6c757d;">' + fileType + ' • ' + fileSize + ' • Created: ' + file.created + '</small>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<a href="' + BASE_PATH + (file.url.startsWith('/') ? file.url.substring(1) : file.url) + '" target="_blank" style="background: #007bff; color: white; padding: 8px 16px; text-decoration: none; border-radius: 4px; font-size: 0.9em; flex-shrink: 0; white-space: nowrap;">' +
|
||
'View/Download' +
|
||
'</a>' +
|
||
'</div>';
|
||
|
||
savedFilesList.appendChild(fileDiv);
|
||
|
||
// Remove highlight after 5 seconds for latest file
|
||
if (isLatest) {
|
||
setTimeout(() => {
|
||
fileDiv.style.background = 'white';
|
||
fileDiv.style.border = '1px solid #dee2e6';
|
||
fileDiv.style.boxShadow = 'none';
|
||
const newBadgeElement = fileDiv.querySelector('[style*="background: #28a745"]');
|
||
if (newBadgeElement) {
|
||
newBadgeElement.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
});
|
||
|
||
savedFilesContainer.style.display = 'block';
|
||
}
|
||
|
||
function updateConsolidateControls() {
|
||
const checkboxes = document.querySelectorAll('.report-select-cb:checked');
|
||
const controls = document.getElementById('consolidateControls');
|
||
const countEl = document.getElementById('consolidateCount');
|
||
const btn = document.getElementById('consolidateBtn');
|
||
const count = checkboxes.length;
|
||
|
||
if (count >= 1) {
|
||
controls.style.display = 'flex';
|
||
countEl.textContent = count + ' report' + (count !== 1 ? 's' : '') + ' selected';
|
||
btn.disabled = count < 2;
|
||
} else {
|
||
controls.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function clearFileSelection() {
|
||
document.querySelectorAll('.report-select-cb').forEach(cb => cb.checked = false);
|
||
updateConsolidateControls();
|
||
}
|
||
|
||
async function deleteSelectedReports() {
|
||
const checkboxes = document.querySelectorAll('.report-select-cb:checked');
|
||
if (checkboxes.length === 0) return;
|
||
|
||
const count = checkboxes.length;
|
||
if (!confirm(`Delete ${count} report${count !== 1 ? 's' : ''}? This cannot be undone.`)) return;
|
||
|
||
const filenames = Array.from(checkboxes).map(cb => cb.dataset.filename);
|
||
const client = selectedClient;
|
||
|
||
const btn = document.getElementById('deleteSelectedBtn');
|
||
btn.textContent = 'Deleting...';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}api/delete_output_files`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ filenames, client })
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.deleted_count > 0) {
|
||
console.log(`Deleted ${result.deleted_count} file(s)`);
|
||
await loadSavedFiles();
|
||
}
|
||
if (result.errors && result.errors.length > 0) {
|
||
console.warn('Some files could not be deleted:', result.errors);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting files:', error);
|
||
alert('Failed to delete files. Please try again.');
|
||
} finally {
|
||
btn.textContent = 'Delete Selected';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function consolidateSelectedReports() {
|
||
const checkboxes = document.querySelectorAll('.report-select-cb:checked');
|
||
if (checkboxes.length < 2) return;
|
||
|
||
const files = Array.from(checkboxes).map(cb => ({
|
||
filename: cb.dataset.filename,
|
||
client: cb.dataset.client
|
||
}));
|
||
|
||
const btn = document.getElementById('consolidateBtn');
|
||
btn.textContent = 'Generating...';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}api/consolidate_reports`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ files })
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success') {
|
||
// Open consolidated report in new tab
|
||
const reportUrl = BASE_PATH + (data.url.startsWith('/') ? data.url.substring(1) : data.url);
|
||
window.open(reportUrl, '_blank');
|
||
// Refresh file list
|
||
clearFileSelection();
|
||
loadSavedFiles(true);
|
||
} else {
|
||
alert('Consolidation failed: ' + (data.message || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Error consolidating reports:', error);
|
||
alert('Error consolidating reports: ' + error.message);
|
||
} finally {
|
||
btn.textContent = 'Consolidate Reports';
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Populate profile select dropdown
|
||
function populateProfileSelect() {
|
||
console.log('FUNCTION START - populateProfileSelect');
|
||
console.log('=== POPULATE PROFILE SELECT CALLED ===');
|
||
|
||
try {
|
||
// Use fresh lookup to ensure we have the element
|
||
const profileDropdown = profileSelect || document.getElementById('profile-select');
|
||
console.log('Global profileSelect:', profileSelect);
|
||
console.log('Fresh lookup result:', document.getElementById('profile-select'));
|
||
console.log('Profile dropdown element to use:', profileDropdown);
|
||
console.log('Available profiles for population:', availableProfiles);
|
||
console.log('Available profiles keys:', Object.keys(availableProfiles || {}));
|
||
|
||
if (!profileDropdown) {
|
||
console.error('Profile select element not found!');
|
||
return;
|
||
}
|
||
|
||
profileDropdown.innerHTML = '<option value="">Select a profile...</option>';
|
||
|
||
if (!availableProfiles || Object.keys(availableProfiles).length === 0) {
|
||
console.warn('No profiles available to populate');
|
||
profileDropdown.innerHTML = '<option value="">No profiles available</option>';
|
||
return;
|
||
}
|
||
|
||
for (const [profileId, profileInfo] of Object.entries(availableProfiles)) {
|
||
console.log(`Adding profile: ${profileId}`, profileInfo);
|
||
const option = document.createElement('option');
|
||
option.value = profileId;
|
||
option.textContent = `${profileInfo.name} (${profileInfo.enabled_count} checks)`;
|
||
profileDropdown.appendChild(option);
|
||
}
|
||
|
||
console.log('Profile select populated with', profileDropdown.children.length - 1, 'profiles');
|
||
|
||
} catch (error) {
|
||
console.error('Error in populateProfileSelect:', error);
|
||
}
|
||
}
|
||
|
||
// Setup event listeners
|
||
function setupEventListeners() {
|
||
console.log('Setting up event listeners...');
|
||
console.log('Elements check:', {
|
||
fileUploadArea: !!fileUploadArea,
|
||
fileInput: !!fileInput,
|
||
profileSelect: !!profileSelect,
|
||
analyzeBtn: !!analyzeBtn,
|
||
settingsBtn: !!document.getElementById('settingsBtn')
|
||
});
|
||
|
||
// File upload area
|
||
fileUploadArea.addEventListener('click', () => fileInput.click());
|
||
fileUploadArea.addEventListener('dragover', handleDragOver);
|
||
fileUploadArea.addEventListener('dragleave', handleDragLeave);
|
||
fileUploadArea.addEventListener('drop', handleFileDrop);
|
||
|
||
// File input change
|
||
fileInput.addEventListener('change', handleFileSelect);
|
||
|
||
// Profile change
|
||
profileSelect.addEventListener('change', checkFormValidity);
|
||
|
||
// Analyze button
|
||
analyzeBtn.addEventListener('click', startAnalysis);
|
||
|
||
// Settings button
|
||
const settingsBtn = document.getElementById('settingsBtn');
|
||
if (settingsBtn) {
|
||
console.log('Adding click listener to settings button');
|
||
settingsBtn.addEventListener('click', showSettings);
|
||
} else {
|
||
console.error('Settings button not found!');
|
||
}
|
||
|
||
// Update cost display when profile changes
|
||
profileSelect.addEventListener('change', updateCostDisplay);
|
||
|
||
console.log('Event listeners setup complete');
|
||
}
|
||
|
||
// Handle drag and drop
|
||
function handleDragOver(e) {
|
||
e.preventDefault();
|
||
fileUploadArea.classList.add('dragover');
|
||
}
|
||
|
||
function handleDragLeave(e) {
|
||
e.preventDefault();
|
||
fileUploadArea.classList.remove('dragover');
|
||
}
|
||
|
||
function handleFileDrop(e) {
|
||
e.preventDefault();
|
||
fileUploadArea.classList.remove('dragover');
|
||
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
const filesArray = Array.from(files);
|
||
if (filesArray.length === 1) {
|
||
selectedFile = filesArray[0];
|
||
displayFileInfo(filesArray[0]);
|
||
checkFormValidity();
|
||
} else {
|
||
addFilesToQueue(filesArray);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle file selection
|
||
function handleFileSelect(e) {
|
||
const files = Array.from(e.target.files);
|
||
if (files.length > 0) {
|
||
if (files.length === 1) {
|
||
// Single file - maintain existing behavior
|
||
selectedFile = files[0];
|
||
displayFileInfo(files[0]);
|
||
checkFormValidity();
|
||
} else {
|
||
// Multiple files - add to queue
|
||
addFilesToQueue(files);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Display file information
|
||
function displayFileInfo(file) {
|
||
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||
fileInfo.innerHTML = `
|
||
<div style="background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745;">
|
||
<strong>Selected:</strong> ${file.name}<br>
|
||
<strong>Size:</strong> ${fileSize} MB<br>
|
||
<strong>Type:</strong> ${file.type || 'Unknown'}<br>
|
||
<button onclick="detectBrandAndType()" style="background: #17a2b8; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; margin-top: 8px; font-size: 0.9em;">🔍 Detect Brand & Type</button>
|
||
<div id="detectionResults" style="margin-top: 10px; display: none;"></div>
|
||
</div>
|
||
`;
|
||
fileInfo.style.display = 'block';
|
||
}
|
||
|
||
// Add files to queue for batch processing
|
||
function addFilesToQueue(files) {
|
||
files.forEach(file => {
|
||
fileQueue.push({
|
||
file: file,
|
||
status: 'pending',
|
||
result: null
|
||
});
|
||
});
|
||
displayQueue();
|
||
checkFormValidity();
|
||
}
|
||
|
||
// Display the file queue
|
||
function displayQueue() {
|
||
const queueContainer = document.getElementById('fileQueue');
|
||
const queueList = document.getElementById('queueList');
|
||
const queueCount = document.getElementById('queueCount');
|
||
|
||
if (fileQueue.length === 0) {
|
||
queueContainer.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
queueContainer.style.display = 'block';
|
||
queueCount.textContent = fileQueue.length;
|
||
|
||
queueList.innerHTML = fileQueue.map((item, index) => {
|
||
const fileSize = (item.file.size / 1024 / 1024).toFixed(2);
|
||
let statusColor = '#6c757d';
|
||
let statusText = 'Pending';
|
||
let statusIcon = '⏳';
|
||
|
||
if (item.status === 'analyzing') {
|
||
statusColor = '#007bff';
|
||
statusText = 'Analyzing...';
|
||
statusIcon = '🔄';
|
||
} else if (item.status === 'analyzed') {
|
||
statusColor = '#28a745';
|
||
statusText = 'Complete';
|
||
statusIcon = '✅';
|
||
} else if (item.status === 'error') {
|
||
statusColor = '#dc3545';
|
||
statusText = 'Error';
|
||
statusIcon = '❌';
|
||
}
|
||
|
||
return `
|
||
<div style="background: #f8f9fa; padding: 12px; margin-bottom: 10px; border-radius: 6px; border-left: 4px solid ${statusColor};">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 12px;">
|
||
<div style="flex: 1; min-width: 0; overflow: hidden;">
|
||
<strong style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${item.file.name}</strong>
|
||
<small style="color: #6c757d;">${fileSize} MB | ${item.file.type || 'Unknown type'}</small>
|
||
</div>
|
||
<div style="text-align: right; flex-shrink: 0;">
|
||
<span style="background: ${statusColor}; color: white; padding: 4px 12px; border-radius: 4px; font-size: 0.85em;">
|
||
${statusIcon} ${statusText}
|
||
</span>
|
||
${item.status === 'pending' ? `
|
||
<button onclick="removeFromQueue(${index})" style="background: #dc3545; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; margin-left: 8px; font-size: 0.85em;">Remove</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Remove a file from queue
|
||
function removeFromQueue(index) {
|
||
fileQueue.splice(index, 1);
|
||
displayQueue();
|
||
checkFormValidity();
|
||
}
|
||
|
||
// Clear all files from queue
|
||
function clearQueue() {
|
||
if (fileQueue.length > 0) {
|
||
if (confirm(`Remove all ${fileQueue.length} files from queue?`)) {
|
||
fileQueue = [];
|
||
displayQueue();
|
||
checkFormValidity();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Process all files in queue
|
||
async function processQueue() {
|
||
if (!profileSelect.value) {
|
||
alert('Please select a QC profile before processing the queue.');
|
||
return;
|
||
}
|
||
|
||
const pendingFiles = fileQueue.filter(item => item.status === 'pending');
|
||
if (pendingFiles.length === 0) {
|
||
alert('No pending files to process.');
|
||
return;
|
||
}
|
||
|
||
// Show progress
|
||
progressContainer.style.display = 'block';
|
||
resultsContainer.style.display = 'none';
|
||
|
||
let successCount = 0;
|
||
let errorCount = 0;
|
||
|
||
for (let i = 0; i < fileQueue.length; i++) {
|
||
const item = fileQueue[i];
|
||
|
||
if (item.status !== 'pending') continue;
|
||
|
||
try {
|
||
// Update status
|
||
item.status = 'analyzing';
|
||
displayQueue();
|
||
|
||
// Update progress display
|
||
document.getElementById('progressTitle').textContent = `PROCESSING FILE ${successCount + errorCount + 1} OF ${pendingFiles.length}`;
|
||
document.getElementById('progressSubtitle').textContent = item.file.name;
|
||
currentApp.textContent = 'Analyzing...';
|
||
progressFill.style.width = `${((successCount + errorCount) / pendingFiles.length) * 100}%`;
|
||
|
||
// Prepare form data
|
||
const formData = new FormData();
|
||
formData.append('file', item.file);
|
||
formData.append('profile', profileSelect.value);
|
||
formData.append('mode', outputMode.value);
|
||
|
||
// Add model override if selected
|
||
if (selectedModel) {
|
||
formData.append('model_version', availableModels[selectedModel].model_id);
|
||
}
|
||
|
||
// Add selected reference asset if any
|
||
const referenceAssetSelect = document.getElementById('reference-asset-select');
|
||
if (referenceAssetSelect && referenceAssetSelect.value) {
|
||
formData.append('reference_asset', referenceAssetSelect.value);
|
||
}
|
||
|
||
// Add media plan selection
|
||
const mediaPlanSelect = document.getElementById('media-plan-select');
|
||
if (mediaPlanSelect && mediaPlanSelect.value) {
|
||
formData.append('use_media_plan', 'true');
|
||
}
|
||
|
||
// Perform analysis
|
||
const result = await performAnalysisWithProgress(formData);
|
||
|
||
item.status = 'analyzed';
|
||
item.result = result;
|
||
successCount++;
|
||
|
||
} catch (error) {
|
||
console.error(`Error processing ${item.file.name}:`, error);
|
||
item.status = 'error';
|
||
item.result = error.message;
|
||
errorCount++;
|
||
}
|
||
|
||
displayQueue();
|
||
}
|
||
|
||
// Update progress to complete
|
||
progressFill.style.width = '100%';
|
||
document.getElementById('progressTitle').textContent = 'QUEUE COMPLETE';
|
||
document.getElementById('progressSubtitle').textContent = `Processed ${successCount} of ${successCount + errorCount}${errorCount > 0 ? `, ${errorCount} error${errorCount > 1 ? 's' : ''}` : ''}`;
|
||
|
||
// Hide progress after delay
|
||
setTimeout(() => {
|
||
progressContainer.style.display = 'none';
|
||
}, 3000);
|
||
|
||
// Show summary
|
||
alert(`Queue processing complete!\n\nProcessed ${successCount} of ${successCount + errorCount}${errorCount > 0 ? `\n❌ Processing errors: ${errorCount}` : ''}\n\nCheck the "Saved QC Files" section for results.`);
|
||
|
||
// Reset queue items to pending so user can reprocess (e.g. with different model)
|
||
fileQueue.forEach(item => {
|
||
item.status = 'pending';
|
||
item.result = null;
|
||
});
|
||
displayQueue();
|
||
|
||
// Refresh saved files list
|
||
loadSavedFiles();
|
||
checkFormValidity();
|
||
}
|
||
|
||
// Check if form is valid and update cost
|
||
function checkFormValidity() {
|
||
// START QC ANALYSIS button - only for single file uploads
|
||
const hasSingleFile = selectedFile !== null && fileQueue.length === 0;
|
||
analyzeBtn.disabled = !(hasSingleFile && profileSelect.value);
|
||
|
||
// Process Queue button - only for pending files in queue
|
||
const processBtn = document.getElementById('processQueueBtn');
|
||
if (processBtn) {
|
||
const hasPendingFiles = fileQueue.some(item => item.status === 'pending');
|
||
processBtn.disabled = !(hasPendingFiles && profileSelect.value);
|
||
}
|
||
|
||
updateCostDisplay();
|
||
}
|
||
|
||
// Update cost display based on selected profile
|
||
function updateCostDisplay() {
|
||
const costDisplay = document.getElementById('costDisplay');
|
||
const estimatedCost = document.getElementById('estimatedCost');
|
||
const checkCount = document.getElementById('checkCount');
|
||
|
||
if (profileSelect.value && availableProfiles[profileSelect.value]) {
|
||
const profile = availableProfiles[profileSelect.value];
|
||
const numChecks = profile.enabled_count;
|
||
const cost = numChecks * 0.01; // Updated based on actual usage: ~$0.01 per check
|
||
|
||
estimatedCost.textContent = `$${cost.toFixed(2)}`;
|
||
checkCount.textContent = numChecks;
|
||
costDisplay.style.display = 'block';
|
||
} else {
|
||
costDisplay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Start analysis
|
||
async function startAnalysis() {
|
||
if (!selectedFile || !profileSelect.value) {
|
||
alert('Please select a file and profile before starting analysis.');
|
||
return;
|
||
}
|
||
|
||
// Show progress
|
||
progressContainer.style.display = 'block';
|
||
resultsContainer.style.display = 'none';
|
||
analyzeBtn.disabled = true;
|
||
|
||
try {
|
||
// Prepare form data
|
||
const formData = new FormData();
|
||
formData.append('file', selectedFile);
|
||
formData.append('profile', profileSelect.value);
|
||
console.log('DEBUG: outputMode.value =', outputMode.value);
|
||
formData.append('mode', outputMode.value);
|
||
|
||
// Add model override if selected
|
||
if (selectedModel) {
|
||
formData.append('model_version', availableModels[selectedModel].model_id);
|
||
console.log('Using model override:', availableModels[selectedModel].model_id);
|
||
}
|
||
|
||
// Add selected reference asset if any
|
||
const referenceAssetSelect = document.getElementById('reference-asset-select');
|
||
if (referenceAssetSelect && referenceAssetSelect.value) {
|
||
formData.append('reference_asset', referenceAssetSelect.value);
|
||
}
|
||
|
||
// Add media plan selection
|
||
const mediaPlanSelect = document.getElementById('media-plan-select');
|
||
if (mediaPlanSelect && mediaPlanSelect.value) {
|
||
formData.append('use_media_plan', 'true');
|
||
}
|
||
|
||
// Start analysis with progress tracking
|
||
const analysisResult = await performAnalysisWithProgress(formData);
|
||
|
||
// Handle the results
|
||
await handleAnalysisResults(analysisResult);
|
||
|
||
} catch (error) {
|
||
console.error('Analysis error:', error);
|
||
showError('Analysis failed: ' + error.message);
|
||
} finally {
|
||
progressContainer.style.display = 'none';
|
||
analyzeBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Perform analysis with progress tracking
|
||
async function performAnalysisWithProgress(formData) {
|
||
document.getElementById('progressTitle').textContent = 'PROCESSING';
|
||
document.getElementById('progressSubtitle').textContent = 'Initializing analysis...';
|
||
currentApp.textContent = 'Starting quality control analysis...';
|
||
progressFill.style.width = '5%';
|
||
|
||
try {
|
||
console.log('Starting analysis with profile:', formData.get('profile'));
|
||
console.log('Output mode:', formData.get('mode'));
|
||
|
||
// Capture the original output mode for later use
|
||
const originalMode = formData.get('mode');
|
||
|
||
// Convert profile name to brand for the API
|
||
const profile = formData.get('profile');
|
||
if (profile && profile !== 'general') {
|
||
const brand = profile.split('_')[0];
|
||
formData.set('brand', brand);
|
||
}
|
||
|
||
// Keep the original mode selection - our async analysis supports progress tracking for both JSON and HTML modes
|
||
|
||
// Start the analysis and get session ID for progress tracking
|
||
progressFill.style.width = '10%';
|
||
currentApp.textContent = 'Submitting file for analysis...';
|
||
|
||
const response = await fetch(`${BASE_PATH}api/start_analysis`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
credentials: 'include'
|
||
});
|
||
|
||
console.log('Analysis response status:', response.status);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Analysis failed with status ${response.status}`);
|
||
}
|
||
|
||
// Check if we got a session_id for progress tracking
|
||
const responseText = await response.text();
|
||
console.log('Raw analysis response:', responseText);
|
||
|
||
let responseData;
|
||
try {
|
||
responseData = JSON.parse(responseText);
|
||
} catch (parseError) {
|
||
console.error('Failed to parse analysis response as JSON:', parseError);
|
||
console.error('Response was:', responseText);
|
||
throw new Error('Invalid JSON response from analysis endpoint');
|
||
}
|
||
|
||
console.log('Analysis response data:', responseData);
|
||
console.log('Session ID in response:', responseData.session_id);
|
||
|
||
if (responseData.session_id) {
|
||
console.log('Using real-time progress tracking for session:', responseData.session_id);
|
||
// We have a session ID, use real-time progress tracking
|
||
await trackAnalysisProgress(responseData.session_id);
|
||
|
||
// Set the original output mode back for final handling
|
||
responseData.original_mode = originalMode;
|
||
|
||
// Get final results
|
||
return responseData;
|
||
} else {
|
||
console.log('No session ID, analysis completed immediately');
|
||
// No session ID, use the response directly
|
||
progressFill.style.width = '100%';
|
||
document.getElementById('progressTitle').textContent = 'PROCESSING COMPLETE';
|
||
document.getElementById('progressSubtitle').textContent = 'Analysis complete!';
|
||
currentApp.textContent = 'Analysis complete!';
|
||
|
||
// Set the original output mode back for final handling
|
||
responseData.original_mode = originalMode;
|
||
|
||
return responseData;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Analysis error in performAnalysisWithProgress:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Track analysis progress using session ID
|
||
async function trackAnalysisProgress(sessionId) {
|
||
console.log('Starting progress tracking for session:', sessionId);
|
||
return new Promise((resolve, reject) => {
|
||
const pollProgress = async () => {
|
||
try {
|
||
console.log(`Polling progress for session: ${sessionId}`);
|
||
const response = await fetch(`${BASE_PATH}api/progress/${sessionId}`, {
|
||
credentials: 'include'
|
||
});
|
||
console.log('Progress API response status:', response.status);
|
||
if (!response.ok) {
|
||
console.error(`Progress check failed with status: ${response.status}`);
|
||
const errorText = await response.text();
|
||
console.error('Error response:', errorText);
|
||
throw new Error(`Progress check failed: ${response.status}`);
|
||
}
|
||
|
||
const responseText = await response.text();
|
||
console.log('Raw progress response:', responseText);
|
||
|
||
let data;
|
||
try {
|
||
data = JSON.parse(responseText);
|
||
} catch (parseError) {
|
||
console.error('Failed to parse progress response as JSON:', parseError);
|
||
console.error('Response was:', responseText);
|
||
throw new Error('Invalid JSON response from progress endpoint');
|
||
}
|
||
|
||
console.log('Progress data received:', data);
|
||
const progress = data.progress;
|
||
|
||
console.log('Progress update:', progress);
|
||
|
||
// Update progress display
|
||
const percentage = progress.percentage || 0;
|
||
const stage = progress.stage || 'processing';
|
||
const currentCheck = progress.current_check || 'Processing';
|
||
const currentCheckDisplay = progress.current_check_display || currentCheck;
|
||
const completedChecks = progress.completed_checks || 0;
|
||
const totalChecks = progress.total_checks || 1;
|
||
|
||
// Update progress bar
|
||
progressFill.style.width = `${percentage}%`;
|
||
|
||
// Update progress text
|
||
if (stage === 'complete') {
|
||
document.getElementById('progressTitle').textContent = 'PROCESSING COMPLETE';
|
||
document.getElementById('progressSubtitle').textContent = 'Analysis complete!';
|
||
currentApp.textContent = 'All quality control checks completed successfully!';
|
||
// Return the analysis results
|
||
resolve(progress.result || data);
|
||
} else {
|
||
document.getElementById('progressTitle').textContent = 'PROCESSING';
|
||
document.getElementById('progressSubtitle').textContent = `Step ${completedChecks + 1} of ${totalChecks}`;
|
||
currentApp.textContent = `${currentCheckDisplay}`;
|
||
}
|
||
|
||
// Continue polling if not complete
|
||
if (stage !== 'complete' && stage !== 'error') {
|
||
setTimeout(pollProgress, 1000); // Poll every second
|
||
} else if (stage === 'error') {
|
||
reject(new Error(progress.error || 'Analysis failed'));
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Progress polling error:', error);
|
||
reject(error);
|
||
}
|
||
};
|
||
|
||
// Start polling immediately
|
||
pollProgress();
|
||
});
|
||
}
|
||
|
||
// Handle analysis completion - process results and trigger download
|
||
async function handleAnalysisResults(analysisResult) {
|
||
console.log('=== HANDLING ANALYSIS RESULTS ===');
|
||
console.log('Analysis result:', analysisResult);
|
||
|
||
// Display results in the UI first
|
||
console.log('Displaying results in UI...');
|
||
displayResults(analysisResult);
|
||
|
||
// Auto-generate and download HTML report if originally requested HTML mode
|
||
if (analysisResult.status === 'success') {
|
||
if (analysisResult.original_mode === 'html' || analysisResult.session_id) {
|
||
console.log('Analysis successful, generating HTML report...');
|
||
await generateHtmlReport(analysisResult);
|
||
} else {
|
||
console.log('JSON mode requested, skipping HTML report generation');
|
||
}
|
||
} else {
|
||
console.warn('Analysis was not successful, skipping HTML report generation');
|
||
}
|
||
|
||
// Refresh saved files list with delay to ensure file is written
|
||
console.log('Refreshing saved files list...');
|
||
await refreshSavedFilesAfterAnalysis();
|
||
console.log('=== ANALYSIS RESULTS HANDLING COMPLETE ===');
|
||
}
|
||
|
||
// Generate HTML report and trigger download
|
||
async function generateHtmlReport(analysisResult) {
|
||
try {
|
||
console.log('Generating HTML report...');
|
||
console.log('Analysis result for HTML generation:', analysisResult);
|
||
|
||
// Check if the analysis result contains output file info
|
||
if (analysisResult.output_file && analysisResult.output_file.auto_saved) {
|
||
const outputFile = analysisResult.output_file;
|
||
|
||
// Show success message to user
|
||
showSuccessMessage(`HTML report saved as: ${outputFile.filename}`);
|
||
|
||
console.log('HTML report auto-saved:', outputFile.path);
|
||
} else {
|
||
console.log('No auto-save info found in analysis result, report may have been saved server-side');
|
||
showSuccessMessage('HTML report has been automatically saved to the output directory.');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error with HTML report:', error);
|
||
}
|
||
}
|
||
|
||
// Generate HTML report content from analysis results
|
||
function generateHtmlReportContent(analysisResult) {
|
||
// This function is no longer needed since reports are generated server-side
|
||
return '<html><head><title>Report</title></head><body><h1>Report generated server-side</h1></body></html>';
|
||
}
|
||
|
||
// Display analysis results in the UI
|
||
function displayResults(data) {
|
||
try {
|
||
console.log('=== DISPLAY RESULTS DEBUG START ===');
|
||
console.log('Full data received:', JSON.stringify(data, null, 2));
|
||
|
||
// Validate data structure
|
||
if (!data || typeof data !== 'object') {
|
||
console.error('Invalid data structure:', data);
|
||
showError('Invalid analysis data received');
|
||
return;
|
||
}
|
||
|
||
console.log('Data validation passed');
|
||
|
||
// Extract summary information
|
||
const summary = data.summary || data.qc_analysis || {};
|
||
const qcAnalysis = data.qc_analysis || {};
|
||
const overallScore = summary.overall_score || 0;
|
||
const profileName = data.profile_selection?.suggested_profile || 'Unknown Profile';
|
||
const totalChecks = qcAnalysis.total_checks || Object.keys(qcAnalysis.check_results || {}).length;
|
||
const completedChecks = qcAnalysis.completed_checks || Object.keys(qcAnalysis.check_results || {}).length;
|
||
|
||
console.log('Extracted values:', { overallScore, profileName, totalChecks, completedChecks });
|
||
|
||
// Update results section
|
||
const resultsSection = document.getElementById('results-section');
|
||
if (resultsSection) {
|
||
resultsSection.style.display = 'block';
|
||
|
||
// Update overall score
|
||
const scoreElement = document.getElementById('overall-score');
|
||
if (scoreElement) {
|
||
scoreElement.textContent = overallScore + '/100';
|
||
scoreElement.style.color = overallScore >= 80 ? '#28a745' : overallScore >= 60 ? '#ffc107' : '#dc3545';
|
||
}
|
||
|
||
// Update grade
|
||
const gradeElement = document.getElementById('grade');
|
||
if (gradeElement) {
|
||
const grade = overallScore >= 85 ? 'Excellent' : overallScore >= 70 ? 'Good' : overallScore >= 50 ? 'Adequate' : 'Needs Improvement';
|
||
gradeElement.textContent = grade;
|
||
}
|
||
|
||
// Update checks count
|
||
const checksElement = document.getElementById('checks-count');
|
||
if (checksElement) {
|
||
checksElement.textContent = completedChecks + '/' + totalChecks;
|
||
}
|
||
|
||
// Update profile name
|
||
const profileElement = document.getElementById('profile-used');
|
||
if (profileElement) {
|
||
profileElement.textContent = profileName;
|
||
}
|
||
|
||
console.log('Results UI updated successfully');
|
||
} else {
|
||
console.warn('Results section not found in DOM');
|
||
}
|
||
|
||
// Show download button
|
||
const downloadBtn = document.getElementById('download-results');
|
||
if (downloadBtn) {
|
||
downloadBtn.style.display = 'block';
|
||
downloadBtn.onclick = () => generateHtmlReport(data, data.session_id, data.filename);
|
||
}
|
||
|
||
console.log('=== DISPLAY RESULTS DEBUG END ===');
|
||
|
||
} catch (error) {
|
||
console.error('Error displaying results:', error);
|
||
showError('Failed to display analysis results: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Show error message to user
|
||
function showError(message) {
|
||
console.error('Error:', message);
|
||
|
||
// Try to find an error display element
|
||
let errorElement = document.getElementById('error-message');
|
||
if (!errorElement) {
|
||
// Create error element if it doesn't exist
|
||
errorElement = document.createElement('div');
|
||
errorElement.id = 'error-message';
|
||
errorElement.style.cssText = 'background-color: #f8d7da; color: #721c24; padding: 15px; border: 1px solid #f5c6cb; border-radius: 8px; margin: 20px 0; font-weight: bold;';
|
||
|
||
// Insert at the top of the main container
|
||
const container = document.querySelector('.container') || document.body;
|
||
container.insertBefore(errorElement, container.firstChild);
|
||
}
|
||
|
||
errorElement.textContent = message;
|
||
errorElement.style.display = 'block';
|
||
|
||
// Hide error after 10 seconds
|
||
setTimeout(() => {
|
||
if (errorElement) {
|
||
errorElement.style.display = 'none';
|
||
}
|
||
}, 10000);
|
||
}
|
||
|
||
// Show success message to user
|
||
function showSuccessMessage(message) {
|
||
// Try to find a success display element
|
||
let successElement = document.getElementById('success-message');
|
||
if (!successElement) {
|
||
// Create success element if it doesn't exist
|
||
successElement = document.createElement('div');
|
||
successElement.id = 'success-message';
|
||
successElement.style.cssText = 'background-color: #d4edda; color: #155724; padding: 15px; border: 1px solid #c3e6cb; border-radius: 8px; margin: 20px 0; font-weight: bold;';
|
||
|
||
// Insert at the top of the main container
|
||
const container = document.querySelector('.container') || document.body;
|
||
container.insertBefore(successElement, container.firstChild);
|
||
}
|
||
|
||
successElement.textContent = message;
|
||
successElement.style.display = 'block';
|
||
|
||
// Hide success message after 8 seconds
|
||
setTimeout(() => {
|
||
if (successElement) {
|
||
successElement.style.display = 'none';
|
||
}
|
||
}, 8000);
|
||
}
|
||
|
||
// Settings functionality
|
||
function showSettings() {
|
||
console.log('Settings button clicked');
|
||
// Recreate modal each time to ensure latest content
|
||
let modal = document.getElementById('settingsModal');
|
||
if (modal) {
|
||
modal.remove();
|
||
}
|
||
modal = document.createElement('div');
|
||
modal.id = 'settingsModal';
|
||
modal.innerHTML =
|
||
'<div class="modal-overlay" onclick="closeSettings()">' +
|
||
'<div class="modal-content" onclick="event.stopPropagation()">' +
|
||
'<div class="modal-header">' +
|
||
'<h2>Profile Management</h2>' +
|
||
'<button onclick="closeSettings()" class="close-btn">×</button>' +
|
||
'</div>' +
|
||
'<div class="modal-body">' +
|
||
'<div class="profile-tabs">' +
|
||
'<button class="tab-btn active" onclick="showTab(\'existing\')">Profile</button>' +
|
||
'<button class="tab-btn" onclick="showTab(\'new\')">Create New Profile</button>' +
|
||
'<button class="tab-btn" onclick="showTab(\'assets\')">Reference Assets</button>' +
|
||
'<button class="tab-btn" onclick="showTab(\'tools\')">QC Tools</button>' +
|
||
'<button class="tab-btn" onclick="showTab(\'mediaplan\')">Media Plan</button>' +
|
||
'</div>' +
|
||
'<div id="existing-tab" class="tab-content active">' +
|
||
'<div style="margin-bottom: 15px;">' +
|
||
'<label>Select Profile:</label>' +
|
||
'<select id="profileSelect" onchange="loadSelectedProfile()" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">' +
|
||
'<option value="">Select a profile to edit (or browse all tools below)...</option>' +
|
||
'</select>' +
|
||
'</div>' +
|
||
'<div id="profileEditor" style="display: none;">' +
|
||
'<div style="margin-bottom: 15px;">' +
|
||
'<label>Profile Name:</label>' +
|
||
'<input type="text" id="editProfileName" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc;" readonly>' +
|
||
'</div>' +
|
||
'<div style="margin-bottom: 15px;">' +
|
||
'<label>Profile Description:</label>' +
|
||
'<textarea id="editProfileDescription" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc; height: 60px;"></textarea>' +
|
||
'</div>' +
|
||
'<h4>QC Checks and Weights</h4>' +
|
||
'<div id="editQcChecksList" style="max-height: 300px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px;"></div>' +
|
||
'</div>' +
|
||
'<div id="toolsBrowserSection">' +
|
||
'<div style="margin-bottom: 20px;">' +
|
||
'<h4 style="color: #495057; margin-bottom: 10px;">Available QC Tools</h4>' +
|
||
'<p style="color: #6c757d; font-size: 0.9em; margin-bottom: 15px;">Select a profile above to edit, or review available tools below.</p>' +
|
||
'</div>' +
|
||
'<div style="margin-bottom: 15px;">' +
|
||
'<button onclick="createProfileFromSelectedTools()" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; margin-right: 10px;">Create Profile from Selected Tools</button>' +
|
||
'<button onclick="selectAllTools()" style="background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; margin-right: 10px;">Select All</button>' +
|
||
'<button onclick="deselectAllTools()" style="background: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600;">Deselect All</button>' +
|
||
'</div>' +
|
||
'<div id="toolsDescriptionList" style="max-height: 400px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px;">' +
|
||
'<p style="color: #6c757d; text-align: center;">Loading available tools...</p>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div id="new-tab" class="tab-content">' +
|
||
'<div style="margin-bottom: 15px;">' +
|
||
'<label>Profile Name:</label>' +
|
||
'<input type="text" id="newProfileName" placeholder="Enter profile name..." style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">' +
|
||
'</div>' +
|
||
'<div style="margin-bottom: 15px;">' +
|
||
'<label>Profile Description:</label>' +
|
||
'<textarea id="newProfileDescription" placeholder="Enter profile description..." style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc; height: 60px;"></textarea>' +
|
||
'</div>' +
|
||
'<h4>QC Checks and Weights</h4>' +
|
||
'<div id="newQcChecksList" style="max-height: 300px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px;"></div>' +
|
||
'</div>' +
|
||
'<div id="assets-tab" class="tab-content">' +
|
||
'<div style="margin-bottom: 20px;">' +
|
||
'<h4 style="color: #495057; margin-bottom: 10px;">Upload Reference Asset</h4>' +
|
||
'<p style="color: #6c757d; font-size: 0.9em; margin-bottom: 10px;">Upload a reference asset to enhance QC analysis for your profiles.</p>' +
|
||
'<div id="assetsClientIndicator" style="background: #e3f2fd; border: 1px solid #90caf9; border-radius: 6px; padding: 8px 12px; font-size: 0.85em; color: #1565c0; font-weight: 600;"></div>' +
|
||
'</div>' +
|
||
'<div style="border: 2px dashed #cbd5e0; border-radius: 12px; padding: 25px; background: #f7fafc; margin-bottom: 20px;">' +
|
||
'<div style="text-align: center; margin-bottom: 20px;">' +
|
||
'<div style="font-size: 2.5em; margin-bottom: 10px;">📄</div>' +
|
||
'<p style="margin: 0 0 5px 0; font-size: 1em; font-weight: 600;">Upload a Reference Asset</p>' +
|
||
'<p style="margin: 0; color: #6c757d; font-size: 0.85em;">Supported formats: PDF, Images (JPG, PNG), Excel (XLSX)</p>' +
|
||
'</div>' +
|
||
'<div style="background: #e8f4fd; border: 1px solid #b6d4fe; border-radius: 8px; padding: 12px 15px; margin-bottom: 20px; font-size: 0.85em; color: #0c5460;">' +
|
||
'<strong>How it works:</strong> Once uploaded, this asset is available as a reference during QC analysis. ' +
|
||
'<strong>PDF</strong> files are automatically summarised and the extracted guidelines are included in each check prompt. ' +
|
||
'<strong>Images</strong> are sent alongside the asset being analysed for visual comparison. ' +
|
||
'<strong>Excel</strong> files (e.g. localisation matrices) are parsed for expected copy per market and message type, then cross-referenced with the media plan during analysis to verify the correct text appears in each asset.' +
|
||
'</div>' +
|
||
'<form id="settingsBrandGuidelineForm" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">' +
|
||
'<div>' +
|
||
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Brand Name *</label>' +
|
||
'<input type="text" id="settingsBrandName" placeholder="Enter brand name" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 0.9em;">' +
|
||
'</div>' +
|
||
'<div>' +
|
||
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Tags (Optional)</label>' +
|
||
'<input type="text" id="settingsBrandTags" placeholder="logo, colors, typography" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 0.9em;">' +
|
||
'</div>' +
|
||
'<div style="grid-column: 1 / -1;">' +
|
||
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Description (Optional)</label>' +
|
||
'<textarea id="settingsBrandDescription" placeholder="Brief description of this reference asset" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; height: 50px; resize: vertical; font-size: 0.9em;"></textarea>' +
|
||
'</div>' +
|
||
'</form>' +
|
||
'<div style="text-align: center;">' +
|
||
'<input type="file" id="settingsReferenceFileInput" accept=".pdf,.jpg,.png,.gif,.jpeg,.xlsx,.xls" style="display: none;">' +
|
||
'<button onclick="document.getElementById(\'settingsReferenceFileInput\').click()" style="background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; margin-right: 10px; font-size: 0.9em;">Choose File</button>' +
|
||
'<button onclick="uploadSettingsBrandGuideline()" id="settingsUploadBtn" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.9em;" disabled>Upload Reference Asset</button>' +
|
||
'</div>' +
|
||
'<div id="settingsSelectedFileInfo" style="margin-top: 15px; padding: 10px; background: #d4edda; border-radius: 6px; display: none;"></div>' +
|
||
'</div>' +
|
||
'<div id="settingsGuidelinesList" style="max-height: 300px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px;">' +
|
||
'<p style="color: #6c757d; text-align: center;">Uploaded reference assets will be displayed here</p>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div id="tools-tab" class="tab-content">' +
|
||
'<div style="margin-bottom: 20px;">' +
|
||
'<h4 style="color: #495057; margin-bottom: 10px;">QC Tools Reference</h4>' +
|
||
'<p style="color: #6c757d; font-size: 0.9em; margin-bottom: 15px;">Browse all available QC tools and their descriptions. Use this as a reference when creating or editing profiles.</p>' +
|
||
'</div>' +
|
||
'<div id="toolsReferenceSearch" style="margin-bottom: 15px;">' +
|
||
'<input type="text" id="toolsSearchInput" oninput="filterToolsReference()" placeholder="Search tools..." style="width: 100%; padding: 10px 14px; border: 2px solid #dee2e6; border-radius: 8px; font-size: 0.95em; font-family: Montserrat, sans-serif;">' +
|
||
'</div>' +
|
||
'<div id="toolsReferenceCount" style="color: #6c757d; font-size: 0.85em; margin-bottom: 10px;"></div>' +
|
||
'<div id="toolsReferenceList" style="max-height: 450px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 8px; padding: 10px;">' +
|
||
'<p style="color: #6c757d; text-align: center;">Loading tools...</p>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div id="mediaplan-tab" class="tab-content">' +
|
||
'<div style="margin-bottom: 20px;">' +
|
||
'<h4 style="color: #495057; margin-bottom: 10px;">Media Plan</h4>' +
|
||
'<p style="color: #6c757d; font-size: 0.9em; margin-bottom: 15px;">Upload a media plan (Excel) to automatically validate asset dimensions and file types during QC. Assets are matched by filename.</p>' +
|
||
'</div>' +
|
||
'<div id="mediaPlanCurrent" style="margin-bottom: 20px;"></div>' +
|
||
'<div style="border: 2px dashed #cbd5e0; border-radius: 12px; padding: 25px; background: #f7fafc; margin-bottom: 15px;">' +
|
||
'<div style="text-align: center; margin-bottom: 15px;">' +
|
||
'<div style="font-size: 2.5em; margin-bottom: 10px;">📊</div>' +
|
||
'<p style="margin: 0 0 5px 0; font-size: 1em; font-weight: 600;">Upload Media Plan</p>' +
|
||
'<p style="margin: 0; color: #6c757d; font-size: 0.85em;">Excel format (.xlsx, .xls)</p>' +
|
||
'</div>' +
|
||
'<div style="text-align: center;">' +
|
||
'<input type="file" id="mediaPlanFileInput" accept=".xlsx,.xls" style="display: none;">' +
|
||
'<button onclick="document.getElementById(\'mediaPlanFileInput\').click()" style="background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600; margin-right: 10px;">Choose File</button>' +
|
||
'<button onclick="uploadMediaPlan()" id="mediaPlanUploadBtn" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600;" disabled>Upload Plan</button>' +
|
||
'</div>' +
|
||
'<div id="mediaPlanFileInfo" style="margin-top: 15px; padding: 10px; background: #d4edda; border-radius: 6px; display: none;"></div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div style="text-align: right; padding-top: 15px; border-top: 1px solid #dee2e6;">' +
|
||
'<button onclick="saveProfile()" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; margin-right: 10px;">Save Profile</button>' +
|
||
'<button onclick="deleteProfile()" id="deleteProfileBtn" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; margin-right: 10px; display: none;">Delete Profile</button>' +
|
||
'<button onclick="closeSettings()" style="background: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer;">Cancel</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
modal.style.cssText =
|
||
'position: fixed;' +
|
||
'top: 0;' +
|
||
'left: 0;' +
|
||
'width: 100%;' +
|
||
'height: 100%;' +
|
||
'z-index: 1000;' +
|
||
'display: none;';
|
||
document.body.appendChild(modal);
|
||
|
||
// Add modal styles
|
||
const modalStyles = document.createElement('style');
|
||
modalStyles.textContent =
|
||
'.modal-overlay {' +
|
||
'position: fixed;' +
|
||
'top: 0;' +
|
||
'left: 0;' +
|
||
'width: 100%;' +
|
||
'height: 100%;' +
|
||
'background: rgba(0,0,0,0.5);' +
|
||
'display: flex;' +
|
||
'justify-content: center;' +
|
||
'align-items: center;' +
|
||
'}' +
|
||
'.modal-content {' +
|
||
'background: white;' +
|
||
'border-radius: 8px;' +
|
||
'min-width: 600px;' +
|
||
'max-width: 800px;' +
|
||
'max-height: 80vh;' +
|
||
'overflow-y: auto;' +
|
||
'}' +
|
||
'.modal-header {' +
|
||
'display: flex;' +
|
||
'justify-content: space-between;' +
|
||
'align-items: center;' +
|
||
'padding: 20px;' +
|
||
'border-bottom: 1px solid #dee2e6;' +
|
||
'}' +
|
||
'.modal-header h2 {' +
|
||
'margin: 0;' +
|
||
'color: #333;' +
|
||
'}' +
|
||
'.close-btn {' +
|
||
'background: none;' +
|
||
'border: none;' +
|
||
'font-size: 24px;' +
|
||
'cursor: pointer;' +
|
||
'color: #666;' +
|
||
'padding: 0;' +
|
||
'width: 30px;' +
|
||
'height: 30px;' +
|
||
'display: flex;' +
|
||
'align-items: center;' +
|
||
'justify-content: center;' +
|
||
'}' +
|
||
'.close-btn:hover {' +
|
||
'color: #333;' +
|
||
'}' +
|
||
'.modal-body {' +
|
||
'padding: 20px;' +
|
||
'}' +
|
||
'.profile-tabs {' +
|
||
'display: flex;' +
|
||
'margin-bottom: 20px;' +
|
||
'border-bottom: 1px solid #dee2e6;' +
|
||
'}' +
|
||
'.tab-btn {' +
|
||
'background: none;' +
|
||
'border: none;' +
|
||
'padding: 10px 20px;' +
|
||
'cursor: pointer;' +
|
||
'border-bottom: 3px solid transparent;' +
|
||
'font-size: 14px;' +
|
||
'color: #666;' +
|
||
'}' +
|
||
'.tab-btn.active {' +
|
||
'color: #007bff;' +
|
||
'border-bottom-color: #007bff;' +
|
||
'}' +
|
||
'.tab-btn:hover {' +
|
||
'color: #007bff;' +
|
||
'}' +
|
||
'.tab-content {' +
|
||
'display: none;' +
|
||
'}' +
|
||
'.tab-content.active {' +
|
||
'display: block;' +
|
||
'}' +
|
||
'.check-item {' +
|
||
'display: grid;' +
|
||
'grid-template-columns: 1fr auto auto auto;' +
|
||
'gap: 10px;' +
|
||
'align-items: center;' +
|
||
'padding: 8px 0;' +
|
||
'border-bottom: 1px solid #f0f0f0;' +
|
||
'}' +
|
||
'.check-item:last-child {' +
|
||
'border-bottom: none;' +
|
||
'}' +
|
||
'.check-name {' +
|
||
'font-weight: 500;' +
|
||
'}' +
|
||
'.weight-input {' +
|
||
'width: 60px;' +
|
||
'padding: 4px;' +
|
||
'border: 1px solid #ccc;' +
|
||
'border-radius: 4px;' +
|
||
'text-align: center;' +
|
||
'}';
|
||
if (!document.getElementById('settingsModalStyles')) {
|
||
modalStyles.id = 'settingsModalStyles';
|
||
document.head.appendChild(modalStyles);
|
||
}
|
||
|
||
// Load profiles and QC apps when showing modal
|
||
loadProfileManagement();
|
||
|
||
// Populate model select dropdown
|
||
populateModelSelect();
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
function closeSettings() {
|
||
const modal = document.getElementById('settingsModal');
|
||
if (modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Profile management functions
|
||
let currentProfiles = {};
|
||
let currentQcApps = {};
|
||
let currentEditProfileScale = 100; // Track the weight scale of the profile being edited
|
||
|
||
async function loadProfileManagement() {
|
||
try {
|
||
// Load profiles and QC apps in parallel
|
||
const [profilesResponse, qcAppsResponse] = await Promise.all([
|
||
fetch(`${BASE_PATH}api/profiles`, { credentials: 'include' }),
|
||
fetch(`${BASE_PATH}api/qc-apps`, { credentials: 'include' })
|
||
]);
|
||
|
||
if (!profilesResponse.ok || !qcAppsResponse.ok) {
|
||
throw new Error('Failed to load profile management data');
|
||
}
|
||
|
||
const profilesData = await profilesResponse.json();
|
||
const qcAppsData = await qcAppsResponse.json();
|
||
|
||
console.log('Settings modal - profiles data:', profilesData);
|
||
console.log('Settings modal - qc apps data:', qcAppsData);
|
||
|
||
currentProfiles = profilesData.all_profiles || {};
|
||
currentQcApps = qcAppsData.qc_apps || {};
|
||
|
||
console.log('Settings modal - currentProfiles:', currentProfiles);
|
||
console.log('Settings modal - currentQcApps:', currentQcApps);
|
||
|
||
populateProfileSelect();
|
||
populateQcChecksLists();
|
||
populateToolsDescriptionList();
|
||
|
||
} catch (error) {
|
||
console.error('Error loading profile management:', error);
|
||
showErrorMessage('Failed to load profile management data');
|
||
}
|
||
}
|
||
|
||
function populateProfileSelect() {
|
||
const profileSelect = document.getElementById('profileSelect');
|
||
if (!profileSelect) return;
|
||
|
||
profileSelect.innerHTML = '<option value="">Select a profile...</option>';
|
||
|
||
for (const [profileId, profile] of Object.entries(currentProfiles)) {
|
||
const option = document.createElement('option');
|
||
option.value = profileId;
|
||
option.textContent = profile.name;
|
||
profileSelect.appendChild(option);
|
||
}
|
||
}
|
||
|
||
function populateQcChecksLists() {
|
||
populateChecksForContainer('editQcChecksList');
|
||
populateChecksForContainer('newQcChecksList');
|
||
}
|
||
|
||
function populateChecksForContainer(containerId) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
// Create header
|
||
const headerDiv = document.createElement('div');
|
||
headerDiv.style.cssText = 'display: grid; grid-template-columns: 1fr auto auto auto; gap: 10px; padding: 10px; background: #f8f9fa; border-radius: 6px; margin-bottom: 10px; font-weight: bold; font-size: 0.9em;';
|
||
headerDiv.innerHTML = '<div>QC Check</div><div>Enabled</div><div>Weight</div><div></div>';
|
||
container.appendChild(headerDiv);
|
||
|
||
// Add each QC check
|
||
const showDescriptions = (containerId === 'editQcChecksList');
|
||
for (const [checkName, checkInfo] of Object.entries(currentQcApps)) {
|
||
const checkDiv = document.createElement('div');
|
||
checkDiv.className = 'check-item';
|
||
const descHtml = showDescriptions ? '<div style="grid-column: 1 / -1; color: #6c757d; font-size: 0.8em; line-height: 1.4; padding: 4px 0 2px 0;">' + (checkInfo.description || '') + '</div>' : '';
|
||
checkDiv.innerHTML =
|
||
'<div class="check-name">' + checkInfo.display_name + '</div>' +
|
||
'<input type="checkbox" class="check-enabled" data-check="' + checkName + '">' +
|
||
'<input type="number" class="weight-input" data-check="' + checkName + '" value="0" min="0" step="0.1">' +
|
||
'<div></div>' + descHtml;
|
||
container.appendChild(checkDiv);
|
||
}
|
||
|
||
// Add event listeners to automatically recalculate weights when checkboxes change
|
||
container.querySelectorAll('.check-enabled').forEach(checkbox => {
|
||
checkbox.addEventListener('change', () => recalculateWeights(containerId));
|
||
});
|
||
|
||
// Don't calculate weights initially since nothing is selected by default
|
||
}
|
||
|
||
function recalculateWeights(containerId) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
// Determine which weight scale to use
|
||
// For editQcChecksList, use the profile's custom scale; for newQcChecksList, always use 100
|
||
const weightScale = (containerId === 'editQcChecksList') ? currentEditProfileScale : 100;
|
||
|
||
// Count enabled checks
|
||
const checkboxes = container.querySelectorAll('.check-enabled');
|
||
const enabledCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
||
|
||
if (enabledCount === 0) {
|
||
// If no checks selected, disable all weight inputs
|
||
checkboxes.forEach(checkbox => {
|
||
const checkName = checkbox.dataset.check;
|
||
const weightInput = container.querySelector(`.weight-input[data-check="${checkName}"]`);
|
||
weightInput.value = '0';
|
||
weightInput.disabled = true;
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Calculate even weight distribution based on the profile's weight scale
|
||
const evenWeight = weightScale / enabledCount;
|
||
|
||
// Update weight inputs for enabled checks
|
||
checkboxes.forEach(checkbox => {
|
||
const checkName = checkbox.dataset.check;
|
||
const weightInput = container.querySelector(`.weight-input[data-check="${checkName}"]`);
|
||
|
||
if (checkbox.checked) {
|
||
// Set to even weight, rounded to 2 decimal places
|
||
weightInput.value = evenWeight.toFixed(2);
|
||
weightInput.disabled = false;
|
||
} else {
|
||
// Disabled checks get 0 weight
|
||
weightInput.value = '0';
|
||
weightInput.disabled = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
function populateToolsDescriptionList() {
|
||
const container = document.getElementById('toolsDescriptionList');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
// Sort tools alphabetically by display name
|
||
const sortedTools = Object.entries(currentQcApps).sort((a, b) =>
|
||
a[1].display_name.localeCompare(b[1].display_name)
|
||
);
|
||
|
||
// Add each tool with description
|
||
for (const [checkName, checkInfo] of sortedTools) {
|
||
const toolDiv = document.createElement('div');
|
||
toolDiv.style.cssText = 'border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; margin-bottom: 15px; background: #f8f9fa;';
|
||
|
||
const description = checkInfo.description || 'No description available';
|
||
|
||
toolDiv.innerHTML = `
|
||
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||
<input type="checkbox" class="tool-select" data-check="${checkName}" id="tool-${checkName}" style="margin-right: 10px; width: 18px; height: 18px; cursor: pointer;">
|
||
<label for="tool-${checkName}" style="font-weight: bold; font-size: 1.1em; color: #495057; cursor: pointer; margin: 0;">${checkInfo.display_name}</label>
|
||
</div>
|
||
<div style="color: #6c757d; font-size: 0.9em; line-height: 1.5; padding-left: 28px;">
|
||
${description}
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(toolDiv);
|
||
}
|
||
}
|
||
|
||
function selectAllTools() {
|
||
const checkboxes = document.querySelectorAll('.tool-select');
|
||
checkboxes.forEach(cb => cb.checked = true);
|
||
}
|
||
|
||
function deselectAllTools() {
|
||
const checkboxes = document.querySelectorAll('.tool-select');
|
||
checkboxes.forEach(cb => cb.checked = false);
|
||
}
|
||
|
||
function createProfileFromSelectedTools() {
|
||
// Get selected tools
|
||
const checkboxes = document.querySelectorAll('.tool-select:checked');
|
||
|
||
if (checkboxes.length === 0) {
|
||
alert('Please select at least one tool to create a profile');
|
||
return;
|
||
}
|
||
|
||
// Switch to new profile tab
|
||
showTab('new');
|
||
|
||
// Clear profile name and description fields
|
||
document.getElementById('newProfileName').value = '';
|
||
document.getElementById('newProfileDescription').value = '';
|
||
|
||
// Clear existing selections
|
||
const newContainer = document.getElementById('newQcChecksList');
|
||
newContainer.querySelectorAll('.check-enabled').forEach(cb => cb.checked = false);
|
||
|
||
// Select the chosen tools
|
||
checkboxes.forEach(checkbox => {
|
||
const checkName = checkbox.dataset.check;
|
||
const targetCheckbox = newContainer.querySelector(`.check-enabled[data-check="${checkName}"]`);
|
||
if (targetCheckbox) {
|
||
targetCheckbox.checked = true;
|
||
}
|
||
});
|
||
|
||
// Recalculate weights
|
||
recalculateWeights('newQcChecksList');
|
||
|
||
// Focus on the profile name field to prompt user to enter name
|
||
setTimeout(() => {
|
||
document.getElementById('newProfileName').focus();
|
||
}, 100);
|
||
|
||
// Show helper message at the top of the form
|
||
const nameField = document.getElementById('newProfileName');
|
||
nameField.placeholder = `Enter profile name (${checkboxes.length} tools selected)`;
|
||
}
|
||
|
||
function showTab(tabName) {
|
||
// Update tab buttons
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelector(`.tab-btn[onclick="showTab('${tabName}')"]`).classList.add('active');
|
||
|
||
// Update tab content
|
||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||
document.getElementById(tabName + '-tab').classList.add('active');
|
||
|
||
// Show/hide delete button - only show when profile is selected in Profile tab
|
||
const deleteBtn = document.getElementById('deleteProfileBtn');
|
||
if (deleteBtn) {
|
||
const profileSelect = document.getElementById('profileSelect');
|
||
deleteBtn.style.display = (tabName === 'existing' && profileSelect && profileSelect.value) ? 'inline-block' : 'none';
|
||
}
|
||
|
||
// Load brand guidelines when assets tab is shown
|
||
if (tabName === 'assets') {
|
||
const indicator = document.getElementById('assetsClientIndicator');
|
||
if (indicator) {
|
||
const clientName = selectedClient ? selectedClient.charAt(0).toUpperCase() + selectedClient.slice(1) : 'All';
|
||
indicator.textContent = 'Viewing assets for: ' + clientName;
|
||
}
|
||
loadSettingsBrandGuidelines();
|
||
// Set up file input handler for settings
|
||
const fileInput = document.getElementById('settingsReferenceFileInput');
|
||
const uploadBtn = document.getElementById('settingsUploadBtn');
|
||
if (fileInput && uploadBtn) {
|
||
fileInput.addEventListener('change', function() {
|
||
uploadBtn.disabled = !this.files.length;
|
||
const fileInfo = document.getElementById('settingsSelectedFileInfo');
|
||
if (this.files.length > 0) {
|
||
const file = this.files[0];
|
||
fileInfo.innerHTML = `<strong>Selected:</strong> ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
|
||
fileInfo.style.display = 'block';
|
||
} else {
|
||
fileInfo.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Populate tools reference when tools tab is shown
|
||
if (tabName === 'tools') {
|
||
populateToolsReference();
|
||
}
|
||
|
||
// Load media plan status when media plan tab is shown
|
||
if (tabName === 'mediaplan') {
|
||
loadMediaPlanStatus();
|
||
setupMediaPlanFileInput();
|
||
}
|
||
}
|
||
|
||
// Populate the read-only QC Tools reference list
|
||
function populateToolsReference() {
|
||
const container = document.getElementById('toolsReferenceList');
|
||
const countEl = document.getElementById('toolsReferenceCount');
|
||
if (!container) return;
|
||
|
||
const sortedTools = Object.entries(currentQcApps).sort((a, b) =>
|
||
a[1].display_name.localeCompare(b[1].display_name)
|
||
);
|
||
|
||
if (sortedTools.length === 0) {
|
||
container.innerHTML = '<p style="color: #6c757d; text-align: center; padding: 20px;">No tools available</p>';
|
||
return;
|
||
}
|
||
|
||
countEl.textContent = sortedTools.length + ' tools available';
|
||
container.innerHTML = '';
|
||
|
||
for (const [checkName, checkInfo] of sortedTools) {
|
||
const toolDiv = document.createElement('div');
|
||
toolDiv.className = 'tools-ref-item';
|
||
toolDiv.dataset.name = (checkInfo.display_name + ' ' + checkName + ' ' + (checkInfo.description || '')).toLowerCase();
|
||
toolDiv.style.cssText = 'border-bottom: 1px solid #eee; padding: 12px 10px; transition: background 0.15s;';
|
||
toolDiv.onmouseover = function() { this.style.background = '#f8f9fa'; };
|
||
toolDiv.onmouseout = function() { this.style.background = 'transparent'; };
|
||
|
||
const description = checkInfo.description || 'No description available';
|
||
toolDiv.innerHTML =
|
||
'<div style="font-weight: 600; color: #333; margin-bottom: 4px;">' + checkInfo.display_name + '</div>' +
|
||
'<div style="color: #495057; font-size: 0.88em; line-height: 1.5;">' + description + '</div>' +
|
||
'<div style="color: #adb5bd; font-size: 0.78em; margin-top: 3px; font-family: monospace;">' + checkName + '</div>';
|
||
|
||
container.appendChild(toolDiv);
|
||
}
|
||
}
|
||
|
||
// Filter tools reference list by search input
|
||
function filterToolsReference() {
|
||
const query = document.getElementById('toolsSearchInput').value.toLowerCase();
|
||
const items = document.querySelectorAll('.tools-ref-item');
|
||
let visible = 0;
|
||
|
||
items.forEach(item => {
|
||
const match = !query || item.dataset.name.includes(query);
|
||
item.style.display = match ? '' : 'none';
|
||
if (match) visible++;
|
||
});
|
||
|
||
const countEl = document.getElementById('toolsReferenceCount');
|
||
if (countEl) {
|
||
countEl.textContent = visible + ' of ' + items.length + ' tools' + (query ? ' matching "' + query + '"' : ' available');
|
||
}
|
||
}
|
||
|
||
// Media Plan functions
|
||
function setupMediaPlanFileInput() {
|
||
const fileInput = document.getElementById('mediaPlanFileInput');
|
||
const uploadBtn = document.getElementById('mediaPlanUploadBtn');
|
||
if (fileInput && uploadBtn) {
|
||
fileInput.addEventListener('change', function() {
|
||
uploadBtn.disabled = !this.files.length;
|
||
const info = document.getElementById('mediaPlanFileInfo');
|
||
if (this.files.length > 0) {
|
||
info.innerHTML = '<strong>Selected:</strong> ' + this.files[0].name + ' (' + (this.files[0].size / 1024 / 1024).toFixed(2) + ' MB)';
|
||
info.style.display = 'block';
|
||
} else {
|
||
info.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
async function loadMediaPlanStatus() {
|
||
const container = document.getElementById('mediaPlanCurrent');
|
||
if (!container || !selectedClient) return;
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + 'api/media_plan?client=' + selectedClient, { credentials: 'include' });
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success' && data.plan) {
|
||
const plan = data.plan;
|
||
const channels = Object.entries(plan.channels || {}).map(([ch, count]) => ch + ': ' + count).join(', ');
|
||
container.innerHTML =
|
||
'<div style="background: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px; padding: 15px;">' +
|
||
'<div style="display: flex; justify-content: space-between; align-items: center;">' +
|
||
'<div>' +
|
||
'<strong style="color: #155724;">Active Media Plan</strong><br>' +
|
||
'<span style="font-size: 0.9em; color: #155724;">' + plan.original_filename + '</span><br>' +
|
||
'<small style="color: #155724;">' + plan.total_assets + ' assets | ' + channels + '</small><br>' +
|
||
'<small style="color: #6c757d;">Uploaded: ' + new Date(plan.upload_date).toLocaleDateString() + '</small>' +
|
||
'</div>' +
|
||
'<button onclick="deleteMediaPlan()" style="background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 0.85em;">Remove</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
} else {
|
||
container.innerHTML = '<p style="color: #6c757d; text-align: center; padding: 10px;">No media plan uploaded for this client</p>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading media plan status:', error);
|
||
container.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
async function uploadMediaPlan() {
|
||
const fileInput = document.getElementById('mediaPlanFileInput');
|
||
if (!fileInput.files.length) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', fileInput.files[0]);
|
||
formData.append('client_id', selectedClient || 'general');
|
||
|
||
const uploadBtn = document.getElementById('mediaPlanUploadBtn');
|
||
uploadBtn.textContent = 'Uploading...';
|
||
uploadBtn.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + 'api/media_plan', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success') {
|
||
showSuccessMessage(data.message);
|
||
fileInput.value = '';
|
||
document.getElementById('mediaPlanFileInfo').style.display = 'none';
|
||
loadMediaPlanStatus();
|
||
updateMediaPlanDropdown();
|
||
} else {
|
||
showError('Upload failed: ' + (data.message || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
showError('Upload failed: ' + error.message);
|
||
} finally {
|
||
uploadBtn.textContent = 'Upload Plan';
|
||
uploadBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function deleteMediaPlan() {
|
||
if (!selectedClient) return;
|
||
if (!confirm('Remove the media plan for this client?')) return;
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + 'api/media_plan/' + selectedClient, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
if (data.status === 'success') {
|
||
showSuccessMessage('Media plan removed');
|
||
loadMediaPlanStatus();
|
||
updateMediaPlanDropdown();
|
||
}
|
||
} catch (error) {
|
||
showError('Delete failed: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function loadSelectedProfile() {
|
||
const profileSelect = document.getElementById('profileSelect');
|
||
const selectedProfileId = profileSelect.value;
|
||
|
||
const profileEditor = document.getElementById('profileEditor');
|
||
const toolsBrowser = document.getElementById('toolsBrowserSection');
|
||
const deleteBtn = document.getElementById('deleteProfileBtn');
|
||
|
||
if (!selectedProfileId) {
|
||
profileEditor.style.display = 'none';
|
||
if (toolsBrowser) toolsBrowser.style.display = 'block';
|
||
deleteBtn.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const profile = currentProfiles[selectedProfileId];
|
||
if (!profile) return;
|
||
|
||
// Hide tools browser when editing a profile
|
||
if (toolsBrowser) toolsBrowser.style.display = 'none';
|
||
|
||
// Store the profile's weight scale (default to 100)
|
||
currentEditProfileScale = profile.weight_scale || 100;
|
||
|
||
// Show editor
|
||
profileEditor.style.display = 'block';
|
||
deleteBtn.style.display = 'inline-block';
|
||
|
||
// Populate profile info
|
||
document.getElementById('editProfileName').value = profile.name;
|
||
document.getElementById('editProfileDescription').value = profile.description || '';
|
||
|
||
// Build check list showing only this profile's checks
|
||
const container = document.getElementById('editQcChecksList');
|
||
container.innerHTML = '';
|
||
|
||
// Header
|
||
const headerDiv = document.createElement('div');
|
||
headerDiv.style.cssText = 'display: grid; grid-template-columns: 1fr auto auto auto; gap: 10px; padding: 10px; background: #f8f9fa; border-radius: 6px; margin-bottom: 10px; font-weight: bold; font-size: 0.9em;';
|
||
headerDiv.innerHTML = '<div>QC Check</div><div>Enabled</div><div>Weight</div><div></div>';
|
||
container.appendChild(headerDiv);
|
||
|
||
// Handle both old structure (enabled_checks/weights) and new structure (checks object)
|
||
const enabledChecks = profile.enabled_checks || Object.keys(profile.checks || {});
|
||
const weights = profile.weights || {};
|
||
|
||
if (profile.checks && !profile.weights) {
|
||
Object.entries(profile.checks).forEach(([checkName, checkConfig]) => {
|
||
weights[checkName] = checkConfig.weight || 0;
|
||
});
|
||
}
|
||
|
||
// Only show checks that belong to this profile
|
||
for (const checkName of enabledChecks) {
|
||
const checkInfo = currentQcApps[checkName] || {};
|
||
const displayName = checkInfo.display_name || checkName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||
const description = checkInfo.description || '';
|
||
const weightVal = (weights[checkName] || 0) * 100;
|
||
|
||
const checkDiv = document.createElement('div');
|
||
checkDiv.className = 'check-item';
|
||
checkDiv.innerHTML =
|
||
'<div class="check-name">' + displayName + '</div>' +
|
||
'<input type="checkbox" class="check-enabled" data-check="' + checkName + '" checked>' +
|
||
'<input type="number" class="weight-input" data-check="' + checkName + '" value="' + weightVal + '" min="0" step="0.1">' +
|
||
'<div></div>' +
|
||
'<div style="grid-column: 1 / -1; color: #6c757d; font-size: 0.8em; line-height: 1.4; padding: 4px 0 2px 0;">' + description + '</div>';
|
||
container.appendChild(checkDiv);
|
||
}
|
||
}
|
||
|
||
async function saveProfile() {
|
||
try {
|
||
const activeTab = document.querySelector('.tab-content.active');
|
||
const isNewProfile = activeTab.id === 'new-tab';
|
||
|
||
let profileName, profileDescription, enabledChecks, weights;
|
||
|
||
if (isNewProfile) {
|
||
profileName = document.getElementById('newProfileName').value.trim();
|
||
profileDescription = document.getElementById('newProfileDescription').value.trim();
|
||
const container = document.getElementById('newQcChecksList');
|
||
enabledChecks = [];
|
||
weights = {};
|
||
|
||
container.querySelectorAll('.check-enabled').forEach(checkbox => {
|
||
if (checkbox.checked) {
|
||
enabledChecks.push(checkbox.dataset.check);
|
||
}
|
||
});
|
||
|
||
container.querySelectorAll('.weight-input').forEach(input => {
|
||
weights[input.dataset.check] = parseFloat(input.value) || 0;
|
||
});
|
||
} else {
|
||
const profileSelect = document.getElementById('profileSelect');
|
||
const selectedProfileId = profileSelect.value;
|
||
if (!selectedProfileId) {
|
||
alert('Please select a profile to edit');
|
||
return;
|
||
}
|
||
|
||
profileName = document.getElementById('editProfileName').value.trim();
|
||
profileDescription = document.getElementById('editProfileDescription').value.trim();
|
||
const container = document.getElementById('editQcChecksList');
|
||
enabledChecks = [];
|
||
weights = {};
|
||
|
||
container.querySelectorAll('.check-enabled').forEach(checkbox => {
|
||
if (checkbox.checked) {
|
||
enabledChecks.push(checkbox.dataset.check);
|
||
}
|
||
});
|
||
|
||
container.querySelectorAll('.weight-input').forEach(input => {
|
||
weights[input.dataset.check] = parseFloat(input.value) || 0;
|
||
});
|
||
}
|
||
|
||
if (!profileName) {
|
||
alert('Please enter a profile name');
|
||
return;
|
||
}
|
||
|
||
if (enabledChecks.length === 0) {
|
||
alert('Please select at least one QC check');
|
||
return;
|
||
}
|
||
|
||
// Build checks object in the format expected by backend
|
||
// Note: Always divide by 100 to convert from UI to backend (50.00 → 0.50)
|
||
const checks = {};
|
||
for (const checkName of enabledChecks) {
|
||
checks[checkName] = {
|
||
weight: (weights[checkName] || 0) / 100,
|
||
llm: "Gemini",
|
||
enabled: true
|
||
};
|
||
}
|
||
|
||
const profileData = {
|
||
name: profileName,
|
||
description: profileDescription,
|
||
checks: checks
|
||
};
|
||
|
||
const endpoint = isNewProfile ? `${BASE_PATH}api/profiles` : `${BASE_PATH}api/profiles/${document.getElementById('profileSelect').value}`;
|
||
const method = isNewProfile ? 'POST' : 'PUT';
|
||
|
||
const response = await fetch(endpoint, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(profileData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to save profile: ${response.status}`);
|
||
}
|
||
|
||
alert('Profile saved successfully!');
|
||
closeSettings();
|
||
|
||
// Refresh the profiles dropdown in the main UI
|
||
await loadProfiles();
|
||
|
||
} catch (error) {
|
||
console.error('Error saving profile:', error);
|
||
alert('Failed to save profile: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function deleteProfile() {
|
||
const profileSelect = document.getElementById('profileSelect');
|
||
const selectedProfileId = profileSelect.value;
|
||
|
||
if (!selectedProfileId) {
|
||
alert('Please select a profile to delete');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('Are you sure you want to delete this profile? This action cannot be undone.')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}api/profiles/${selectedProfileId}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to delete profile: ${response.status}`);
|
||
}
|
||
|
||
alert('Profile deleted successfully!');
|
||
closeSettings();
|
||
|
||
// Refresh the profiles dropdown in the main UI
|
||
await loadProfiles();
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting profile:', error);
|
||
alert('Failed to delete profile: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Brand Guidelines Upload Functions (Settings Modal)
|
||
function uploadSettingsBrandGuideline() {
|
||
const fileInput = document.getElementById('settingsReferenceFileInput');
|
||
const brandName = document.getElementById('settingsBrandName').value;
|
||
const tags = document.getElementById('settingsBrandTags').value;
|
||
const description = document.getElementById('settingsBrandDescription').value;
|
||
|
||
if (!fileInput.files.length) {
|
||
showError('Please select a file to upload');
|
||
return;
|
||
}
|
||
|
||
if (!brandName.trim()) {
|
||
showError('Brand name is required');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', fileInput.files[0]);
|
||
formData.append('brand_name', brandName.trim());
|
||
formData.append('tags', tags.trim());
|
||
formData.append('description', description.trim());
|
||
formData.append('client_id', selectedClient || 'general');
|
||
|
||
// Show uploading state
|
||
const uploadBtn = document.getElementById('settingsUploadBtn');
|
||
const originalText = uploadBtn.textContent;
|
||
uploadBtn.textContent = 'Uploading...';
|
||
uploadBtn.disabled = true;
|
||
|
||
fetch(`${BASE_PATH}api/brand_guidelines`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.status === 'success') {
|
||
const fileType = data.file_record && data.file_record.file_type;
|
||
let successMsg = 'Reference asset uploaded successfully!';
|
||
if (fileType === '.pdf') {
|
||
successMsg = 'PDF uploaded! Processing guidelines summary... This may take 10-30 seconds.';
|
||
} else if (fileType === '.xlsx' || fileType === '.xls') {
|
||
successMsg = 'Excel file uploaded! Parsing localisation matrix...';
|
||
}
|
||
showSuccessMessage(successMsg);
|
||
// Clear form
|
||
fileInput.value = '';
|
||
document.getElementById('settingsBrandName').value = '';
|
||
document.getElementById('settingsBrandTags').value = '';
|
||
document.getElementById('settingsBrandDescription').value = '';
|
||
document.getElementById('settingsSelectedFileInfo').style.display = 'none';
|
||
// Refresh brand guidelines list in settings
|
||
loadSettingsBrandGuidelines();
|
||
// Refresh main page reference assets dropdown to include new file
|
||
updateReferenceAssetsDropdown();
|
||
updateMediaPlanDropdown();
|
||
} else {
|
||
showError('Upload failed: ' + (data.message || 'Unknown error'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Upload error:', error);
|
||
showError('Upload failed: ' + error.message);
|
||
})
|
||
.finally(() => {
|
||
uploadBtn.textContent = originalText;
|
||
uploadBtn.disabled = false;
|
||
});
|
||
}
|
||
|
||
|
||
|
||
// Load brand guidelines for settings modal
|
||
async function loadSettingsBrandGuidelines() {
|
||
try {
|
||
const clientParam = selectedClient ? `?client=${selectedClient}` : '';
|
||
const response = await fetch(`${BASE_PATH}api/brand_guidelines${clientParam}`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
console.log('Settings brand guidelines loaded:', data);
|
||
|
||
const guidelinesList = document.getElementById('settingsGuidelinesList');
|
||
if (!guidelinesList) return;
|
||
|
||
if (!data.brands || Object.keys(data.brands).length === 0) {
|
||
guidelinesList.innerHTML = '<p style="color: #6c757d; text-align: center;">No reference assets uploaded yet</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<h4 style="color: #495057; margin-bottom: 15px;">Uploaded Reference Assets</h4>';
|
||
|
||
for (const [brandName, brandData] of Object.entries(data.brands)) {
|
||
html += `<div style="margin-bottom: 20px;">`;
|
||
html += `<h5 style="color: #495057; margin-bottom: 10px; text-transform: capitalize;">${brandName} (${brandData.guidelines.length} ${brandData.guidelines.length === 1 ? 'file' : 'files'})</h5>`;
|
||
|
||
brandData.guidelines.forEach(guidelineId => {
|
||
const guideline = data.files && data.files[guidelineId];
|
||
if (guideline) {
|
||
let statusBadge = '';
|
||
if (guideline.file_type === '.pdf') {
|
||
if (guideline.processed === true) {
|
||
const pages = guideline.page_count ? ` (${guideline.page_count} pages)` : '';
|
||
statusBadge = `<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 4px; font-size: 0.78em; margin-left: 8px;">Summary ready${pages}</span>`;
|
||
} else if (guideline.processed === 'partial') {
|
||
statusBadge = '<span style="background: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 4px; font-size: 0.78em; margin-left: 8px;">Partial summary</span>';
|
||
} else if (guideline.processed === 'error') {
|
||
statusBadge = '<span style="background: #f8d7da; color: #721c24; padding: 2px 8px; border-radius: 4px; font-size: 0.78em; margin-left: 8px;">Processing failed</span>';
|
||
} else {
|
||
statusBadge = '<span style="background: #e2e3e5; color: #383d41; padding: 2px 8px; border-radius: 4px; font-size: 0.78em; margin-left: 8px;">Processing...</span>';
|
||
}
|
||
} else if (guideline.asset_type === 'localization_matrix') {
|
||
const countries = guideline.localization_countries ? guideline.localization_countries.length : 0;
|
||
const messages = guideline.localization_messages ? guideline.localization_messages.join(', ') : '';
|
||
statusBadge = `<span style="background: #d4edda; color: #155724; padding: 2px 8px; border-radius: 4px; font-size: 0.78em; margin-left: 8px;">Localisation Matrix (${countries} markets)</span>`;
|
||
}
|
||
html += `
|
||
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px; margin-bottom: 8px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div style="flex: 1; min-width: 0;">
|
||
<strong>${guideline.original_filename || 'Unknown file'}</strong>${statusBadge}
|
||
${guideline.description ? `<br><small style="color: #6c757d;">${guideline.description}</small>` : ''}
|
||
${guideline.tags && guideline.tags.length ? `<br><small style="color: #007bff;">Tags: ${guideline.tags.join(', ')}</small>` : ''}
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 10px; margin-left: 10px; flex-shrink: 0;">
|
||
<small style="color: #6c757d;">${new Date(guideline.upload_date).toLocaleDateString()}</small>
|
||
<button onclick="deleteReferenceAsset('${guidelineId}', '${(guideline.original_filename || 'this file').replace(/'/g, "\\'")}')" style="background: none; border: 1px solid #dc3545; color: #dc3545; padding: 3px 10px; border-radius: 4px; cursor: pointer; font-size: 0.78em; font-weight: 600; white-space: nowrap;" onmouseover="this.style.background='#dc3545';this.style.color='white'" onmouseout="this.style.background='none';this.style.color='#dc3545'">Remove</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
});
|
||
html += `</div>`;
|
||
}
|
||
|
||
guidelinesList.innerHTML = html;
|
||
|
||
} catch (error) {
|
||
console.error('Error loading settings brand guidelines:', error);
|
||
const guidelinesList = document.getElementById('settingsGuidelinesList');
|
||
if (guidelinesList) {
|
||
guidelinesList.innerHTML = '<p style="color: #dc3545; text-align: center;">Error loading reference assets</p>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Delete a reference asset
|
||
async function deleteReferenceAsset(fileId, filename) {
|
||
if (!confirm(`Are you sure you want to remove "${filename}"?\n\nThis will permanently delete this reference asset.`)) {
|
||
return;
|
||
}
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}api/brand_guidelines/${fileId}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
if (data.status === 'success') {
|
||
loadSettingsBrandGuidelines();
|
||
// Also refresh the reference asset dropdown in the analysis section
|
||
if (typeof loadBrandGuidelines === 'function') {
|
||
loadBrandGuidelines();
|
||
}
|
||
} else {
|
||
alert('Failed to remove reference asset: ' + (data.message || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting reference asset:', error);
|
||
alert('Error removing reference asset. Please try again.');
|
||
}
|
||
}
|
||
|
||
// Reporting tab functions
|
||
// Main app tab switching (Analysis / Reporting)
|
||
function showMainTab(tabName) {
|
||
document.querySelectorAll('.main-tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelector(`.main-tab-btn[onclick="showMainTab('${tabName}')"]`).classList.add('active');
|
||
document.querySelectorAll('.main-tab-content').forEach(content => content.classList.remove('active'));
|
||
document.getElementById(tabName + 'Tab').classList.add('active');
|
||
|
||
if (tabName === 'clientReporting') {
|
||
// Set default dates if not set
|
||
const startInput = document.getElementById('reportingStartDate');
|
||
const endInput = document.getElementById('reportingEndDate');
|
||
if (!startInput.value) {
|
||
const thirtyDaysAgo = new Date();
|
||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||
startInput.value = thirtyDaysAgo.toISOString().split('T')[0];
|
||
}
|
||
if (!endInput.value) {
|
||
endInput.value = new Date().toISOString().split('T')[0];
|
||
}
|
||
// Update title with client name
|
||
const clientName = selectedClient && availableClients[selectedClient]
|
||
? availableClients[selectedClient].display_name
|
||
: 'All';
|
||
document.getElementById('reportingClientTitle').textContent = 'Usage Report for ' + clientName;
|
||
}
|
||
}
|
||
|
||
// Reset reporting dashboard when switching clients
|
||
function resetClientReporting() {
|
||
const cards = document.getElementById('clientReportingCards');
|
||
const table = document.getElementById('clientReportingTable');
|
||
const empty = document.getElementById('clientReportingEmpty');
|
||
if (cards) cards.style.display = 'none';
|
||
if (table) table.style.display = 'none';
|
||
if (empty) {
|
||
empty.innerHTML = '<div style="font-size: 2.5em; margin-bottom: 15px;">📊</div><p style="font-size: 1.1em;">Click "Load Report" to view usage statistics</p>';
|
||
empty.style.display = 'block';
|
||
}
|
||
// Reset date inputs
|
||
const startInput = document.getElementById('reportingStartDate');
|
||
const endInput = document.getElementById('reportingEndDate');
|
||
if (startInput) startInput.value = '';
|
||
if (endInput) endInput.value = '';
|
||
}
|
||
|
||
// Client-scoped reporting dashboard
|
||
async function loadClientReportingData() {
|
||
const startDate = document.getElementById('reportingStartDate').value;
|
||
const endDate = document.getElementById('reportingEndDate').value;
|
||
const cards = document.getElementById('clientReportingCards');
|
||
const table = document.getElementById('clientReportingTable');
|
||
const empty = document.getElementById('clientReportingEmpty');
|
||
|
||
if (!selectedClient) {
|
||
empty.innerHTML = '<div style="font-size: 2.5em; margin-bottom: 15px;">📊</div><p>Please select a client first</p>';
|
||
empty.style.display = 'block';
|
||
cards.style.display = 'none';
|
||
table.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
empty.innerHTML = '<div style="font-size: 2.5em; margin-bottom: 15px;">⏳</div><p>Loading report...</p>';
|
||
empty.style.display = 'block';
|
||
cards.style.display = 'none';
|
||
table.style.display = 'none';
|
||
|
||
try {
|
||
let url = `${BASE_PATH}api/client_usage_stats?client=${selectedClient}`;
|
||
if (startDate) url += `&start_date=${startDate}`;
|
||
if (endDate) url += `&end_date=${endDate}`;
|
||
|
||
const response = await fetch(url, { credentials: 'include' });
|
||
const data = await response.json();
|
||
|
||
if (data.status !== 'success') {
|
||
throw new Error(data.message || 'Failed to load stats');
|
||
}
|
||
|
||
// Update summary cards
|
||
document.getElementById('crTotalAnalyses').textContent = data.total_analyses;
|
||
document.getElementById('crUniqueUsers').textContent = data.unique_users;
|
||
document.getElementById('crTotalChecks').textContent = data.total_checks;
|
||
document.getElementById('crEstCost').textContent = '$' + data.estimated_cost_usd.toFixed(2);
|
||
cards.style.display = 'grid';
|
||
|
||
// Update table
|
||
const tbody = document.getElementById('clientReportingTableBody');
|
||
if (data.recent && data.recent.length > 0) {
|
||
tbody.innerHTML = data.recent.map(r =>
|
||
'<tr>' +
|
||
'<td style="padding: 10px; border-bottom: 1px solid #eee;">' + r.date + '</td>' +
|
||
'<td style="padding: 10px; border-bottom: 1px solid #eee;">' + r.user + '</td>' +
|
||
'<td style="padding: 10px; border-bottom: 1px solid #eee; text-transform: capitalize;">' + r.profile + '</td>' +
|
||
'<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: center;">' + r.checks + '</td>' +
|
||
'<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: center;">' + (r.score ? Number(r.score).toFixed(2) : '-') + '</td>' +
|
||
'<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: right;">$' + r.cost.toFixed(4) + '</td>' +
|
||
'</tr>'
|
||
).join('');
|
||
table.style.display = 'block';
|
||
empty.style.display = 'none';
|
||
} else {
|
||
tbody.innerHTML = '';
|
||
table.style.display = 'none';
|
||
empty.innerHTML = '<div style="font-size: 2.5em; margin-bottom: 15px;">📊</div><p style="font-size: 1.1em;">No usage data yet for this client and date range</p>';
|
||
empty.style.display = 'block';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading client reporting data:', error);
|
||
cards.style.display = 'none';
|
||
table.style.display = 'none';
|
||
empty.innerHTML = '<div style="font-size: 2.5em; margin-bottom: 15px;">⚠️</div><p>Error loading statistics: ' + error.message + '</p>';
|
||
empty.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// Admin panel functions
|
||
async function checkAdminStatus() {
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}api/admin/check`, { credentials: 'include' });
|
||
const data = await response.json();
|
||
isAdmin = data.is_admin === true;
|
||
const adminBtn = document.getElementById('adminBtn');
|
||
if (adminBtn) {
|
||
adminBtn.style.display = isAdmin ? 'inline-flex' : 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error checking admin status:', error);
|
||
isAdmin = false;
|
||
}
|
||
}
|
||
|
||
function showAdminPanel() {
|
||
// Hide other sections, show admin
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('clientSelector').style.display = 'none';
|
||
document.getElementById('authRequired').style.display = 'none';
|
||
document.getElementById('adminSection').style.display = 'block';
|
||
loadAdminUsers();
|
||
}
|
||
|
||
function hideAdminSection() {
|
||
document.getElementById('adminSection').style.display = 'none';
|
||
// Return to the appropriate screen
|
||
if (selectedClient) {
|
||
document.getElementById('mainApp').style.display = 'block';
|
||
} else {
|
||
showClientSelector();
|
||
}
|
||
}
|
||
|
||
async function loadAdminUsers() {
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}api/admin/users`, { credentials: 'include' });
|
||
const data = await response.json();
|
||
|
||
if (data.status !== 'success') {
|
||
throw new Error(data.message || 'Failed to load users');
|
||
}
|
||
|
||
// Update summary cards
|
||
document.getElementById('adminTotalUsers').textContent = data.total_unique_users;
|
||
document.getElementById('adminTotalAnalyses').textContent = data.total_platform_analyses;
|
||
document.getElementById('adminTotalCost').textContent = '$' + data.total_platform_cost.toFixed(2);
|
||
|
||
// Update user table
|
||
const tbody = document.getElementById('adminUserTableBody');
|
||
if (data.users && data.users.length > 0) {
|
||
tbody.innerHTML = data.users.map(u => {
|
||
const lastActive = u.last_active ? u.last_active.substring(0, 16).replace('T', ' ') : '-';
|
||
const clients = u.clients.map(c => c.charAt(0).toUpperCase() + c.slice(1)).join(', ');
|
||
return '<tr>' +
|
||
'<td>' + (u.name || '-') + '</td>' +
|
||
'<td>' + u.email + '</td>' +
|
||
'<td style="text-align: center;">' + u.total_analyses + '</td>' +
|
||
'<td style="text-align: center;">' + u.total_checks + '</td>' +
|
||
'<td>' + clients + '</td>' +
|
||
'<td>' + lastActive + '</td>' +
|
||
'<td style="text-align: right;">$' + u.total_cost.toFixed(2) + '</td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
} else {
|
||
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 30px; color: #6c757d;">No users found</td></tr>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading admin users:', error);
|
||
document.getElementById('adminUserTableBody').innerHTML =
|
||
'<tr><td colspan="7" style="text-align: center; padding: 30px; color: #dc3545;">Error: ' + error.message + '</td></tr>';
|
||
}
|
||
}
|
||
|
||
// Update reference assets dropdown to show all uploaded files
|
||
async function updateReferenceAssetsDropdown() {
|
||
const referenceAssetSelect = document.getElementById('reference-asset-select');
|
||
|
||
if (!referenceAssetSelect) return;
|
||
|
||
try {
|
||
const clientParam = selectedClient ? `?client=${selectedClient}` : '';
|
||
const response = await fetch(`${BASE_PATH}api/brand_guidelines${clientParam}`, {
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
referenceAssetSelect.innerHTML = '<option value="">No reference asset selected</option>';
|
||
|
||
// Show files filtered by client
|
||
if (data.files && Object.keys(data.files).length > 0) {
|
||
// Group files by brand for better organization
|
||
const filesByBrand = {};
|
||
|
||
for (const [fileId, fileData] of Object.entries(data.files)) {
|
||
const brandName = fileData.brand_name || 'Unknown Brand';
|
||
if (!filesByBrand[brandName]) {
|
||
filesByBrand[brandName] = [];
|
||
}
|
||
filesByBrand[brandName].push({ id: fileId, data: fileData });
|
||
}
|
||
|
||
// Add files organized by brand
|
||
for (const [brandName, files] of Object.entries(filesByBrand)) {
|
||
// Create optgroup for better organization
|
||
const optgroup = document.createElement('optgroup');
|
||
optgroup.label = brandName.charAt(0).toUpperCase() + brandName.slice(1);
|
||
|
||
files.forEach(file => {
|
||
const option = document.createElement('option');
|
||
option.value = file.id;
|
||
option.textContent = file.data.original_filename || 'Unknown file';
|
||
if (file.data.description) {
|
||
option.title = file.data.description; // Show description on hover
|
||
}
|
||
optgroup.appendChild(option);
|
||
});
|
||
|
||
referenceAssetSelect.appendChild(optgroup);
|
||
}
|
||
} else {
|
||
// No files available
|
||
referenceAssetSelect.innerHTML = '<option value="">No reference assets available</option>';
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error loading reference assets:', error);
|
||
referenceAssetSelect.innerHTML = '<option value="">Error loading reference assets</option>';
|
||
}
|
||
}
|
||
|
||
async function updateMediaPlanDropdown() {
|
||
const mediaPlanSelect = document.getElementById('media-plan-select');
|
||
if (!mediaPlanSelect || !selectedClient) return;
|
||
|
||
try {
|
||
const response = await fetch(BASE_PATH + 'api/media_plan?client=' + selectedClient, { credentials: 'include' });
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success' && data.plan) {
|
||
const plan = data.plan;
|
||
const channels = Object.entries(plan.channels || {}).map(([ch, count]) => ch + ': ' + count).join(', ');
|
||
mediaPlanSelect.innerHTML =
|
||
'<option value="">No media plan selected</option>' +
|
||
'<option value="use" selected>' + plan.original_filename + ' (' + plan.total_assets + ' assets)</option>';
|
||
mediaPlanSelect.title = channels;
|
||
} else {
|
||
mediaPlanSelect.innerHTML = '<option value="">No media plan available</option>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading media plan:', error);
|
||
mediaPlanSelect.innerHTML = '<option value="">No media plan available</option>';
|
||
}
|
||
}
|
||
|
||
|
||
// MSAL Authentication Configuration
|
||
const msalConfig = {
|
||
auth: {
|
||
clientId: "9079054c-9620-4757-a256-23413042f1ef",
|
||
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
|
||
// Handle both localhost and 127.0.0.1 for local development
|
||
redirectUri: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
|
||
? 'http://localhost:7183'
|
||
: window.location.origin + window.location.pathname.replace(/\/$/, '')
|
||
},
|
||
cache: {
|
||
cacheLocation: "sessionStorage",
|
||
storeAuthStateInCookie: true,
|
||
}
|
||
};
|
||
|
||
const loginRequest = {
|
||
scopes: ["openid", "profile", "email"],
|
||
prompt: "select_account"
|
||
};
|
||
|
||
// Initialize MSAL instance
|
||
let myMSALObj;
|
||
let currentUser = null;
|
||
let isAuthenticated = false;
|
||
let msalInitialized = false;
|
||
let isSigningIn = false; // Prevent concurrent sign-in attempts
|
||
|
||
try {
|
||
if (typeof msal === 'undefined') {
|
||
throw new Error('MSAL library is not available. Please check your internet connection.');
|
||
}
|
||
myMSALObj = new msal.PublicClientApplication(msalConfig);
|
||
msalInitialized = true;
|
||
console.log('MSAL initialized successfully');
|
||
} catch (error) {
|
||
console.error('Error initializing MSAL:', error);
|
||
msalInitialized = false;
|
||
}
|
||
|
||
// Authentication functions
|
||
async function signIn() {
|
||
if (!msalInitialized || !myMSALObj) {
|
||
console.error('MSAL not initialized properly');
|
||
alert('Authentication system not available. Please check your connection and try again.');
|
||
showLoginButton();
|
||
return;
|
||
}
|
||
|
||
// Prevent concurrent sign-in attempts
|
||
if (isSigningIn) {
|
||
console.log('Sign-in already in progress, ignoring duplicate request');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
isSigningIn = true;
|
||
showAuthLoading();
|
||
|
||
// Clear any pending MSAL interactions
|
||
try {
|
||
localStorage.removeItem('msal.interaction.status');
|
||
sessionStorage.removeItem('msal.interaction.status');
|
||
} catch (e) {
|
||
console.warn('Could not clear MSAL storage:', e);
|
||
}
|
||
|
||
const loginResponse = await myMSALObj.loginPopup(loginRequest);
|
||
console.log('Login successful:', loginResponse);
|
||
|
||
// Send token to server for validation
|
||
const idToken = loginResponse.idToken;
|
||
const response = await fetch(`${BASE_PATH}auth/login`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ token: idToken })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
currentUser = result.user;
|
||
isAuthenticated = true;
|
||
console.log('Authentication successful:', currentUser);
|
||
|
||
// Force full page reload to fetch fresh HTML (bypasses cache)
|
||
// This ensures the client selection screen is displayed
|
||
window.location.reload(true);
|
||
} else {
|
||
throw new Error(result.error || 'Authentication failed');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Sign-in error:', error);
|
||
alert('Authentication failed: ' + error.message);
|
||
showLoginButton();
|
||
} finally {
|
||
isSigningIn = false; // Reset flag on both success and failure
|
||
}
|
||
}
|
||
|
||
async function signOut() {
|
||
try {
|
||
// Clear server-side session
|
||
await fetch(`${BASE_PATH}auth/logout`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
});
|
||
|
||
// Clear client-side tokens (only if MSAL is available)
|
||
if (msalInitialized && myMSALObj) {
|
||
await myMSALObj.logoutPopup({
|
||
postLogoutRedirectUri: window.location.origin + window.location.pathname
|
||
});
|
||
}
|
||
|
||
currentUser = null;
|
||
isAuthenticated = false;
|
||
|
||
// Reload page to reset state
|
||
window.location.reload();
|
||
|
||
} catch (error) {
|
||
console.error('Sign-out error:', error);
|
||
// Even if there's an error, reset the UI
|
||
currentUser = null;
|
||
isAuthenticated = false;
|
||
window.location.reload();
|
||
}
|
||
}
|
||
|
||
let authCheckInterval = null; // Periodic auth check timer
|
||
|
||
async function checkAuthStatus() {
|
||
try {
|
||
showAuthLoading();
|
||
|
||
const response = await fetch(`${BASE_PATH}auth/status`);
|
||
const status = await response.json();
|
||
|
||
if (status.authenticated && status.user) {
|
||
currentUser = status.user;
|
||
isAuthenticated = true;
|
||
updateAuthUI();
|
||
// Start periodic auth check if not already running
|
||
startPeriodicAuthCheck();
|
||
} else {
|
||
isAuthenticated = false;
|
||
stopPeriodicAuthCheck();
|
||
showAuthRequired();
|
||
}
|
||
|
||
return isAuthenticated;
|
||
|
||
} catch (error) {
|
||
console.error('Auth status check failed:', error);
|
||
isAuthenticated = false;
|
||
stopPeriodicAuthCheck();
|
||
showAuthRequired();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Silent auth check - runs periodically without showing loading indicators
|
||
async function silentAuthCheck() {
|
||
try {
|
||
const response = await fetch(`${BASE_PATH}auth/status`);
|
||
const status = await response.json();
|
||
|
||
if (!status.authenticated || !status.user) {
|
||
console.warn('Session expired - prompting re-authentication');
|
||
isAuthenticated = false;
|
||
currentUser = null;
|
||
stopPeriodicAuthCheck();
|
||
showSessionExpiredPrompt();
|
||
}
|
||
} catch (error) {
|
||
console.error('Silent auth check failed:', error);
|
||
// Don't immediately log out on network errors - wait for next check
|
||
}
|
||
}
|
||
|
||
function startPeriodicAuthCheck() {
|
||
if (authCheckInterval) return; // Already running
|
||
// Check every 5 minutes (300000ms)
|
||
authCheckInterval = setInterval(silentAuthCheck, 5 * 60 * 1000);
|
||
console.log('Periodic auth check started (every 5 minutes)');
|
||
}
|
||
|
||
function stopPeriodicAuthCheck() {
|
||
if (authCheckInterval) {
|
||
clearInterval(authCheckInterval);
|
||
authCheckInterval = null;
|
||
console.log('Periodic auth check stopped');
|
||
}
|
||
}
|
||
|
||
function showSessionExpiredPrompt() {
|
||
// Hide main app content
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('userInfo').style.display = 'none';
|
||
|
||
// Show auth required screen with expired message
|
||
const authRequired = document.getElementById('authRequired');
|
||
authRequired.style.display = 'block';
|
||
|
||
// Show a session expired message if not already present
|
||
let expiredMsg = document.getElementById('sessionExpiredMsg');
|
||
if (!expiredMsg) {
|
||
expiredMsg = document.createElement('div');
|
||
expiredMsg.id = 'sessionExpiredMsg';
|
||
expiredMsg.style.cssText = 'background: #fff3cd; color: #856404; border: 1px solid #ffc107; border-radius: 8px; padding: 16px; margin: 16px auto; max-width: 500px; text-align: center; font-size: 14px;';
|
||
expiredMsg.innerHTML = '<strong>Session Expired</strong><br>Your authentication session has timed out. Please sign in again to continue.';
|
||
authRequired.insertBefore(expiredMsg, authRequired.firstChild);
|
||
}
|
||
expiredMsg.style.display = 'block';
|
||
|
||
// Show login button
|
||
showLoginButton();
|
||
}
|
||
|
||
// UI update functions
|
||
function showAuthLoading() {
|
||
document.getElementById('authLoading').style.display = 'block';
|
||
document.getElementById('loginBtn').style.display = 'none';
|
||
document.getElementById('userInfo').style.display = 'none';
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('authRequired').style.display = 'block';
|
||
}
|
||
|
||
function showLoginButton() {
|
||
document.getElementById('authLoading').style.display = 'none';
|
||
document.getElementById('loginBtn').style.display = 'block';
|
||
document.getElementById('userInfo').style.display = 'none';
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('authRequired').style.display = 'block';
|
||
}
|
||
|
||
function updateAuthUI() {
|
||
document.getElementById('authLoading').style.display = 'none';
|
||
document.getElementById('loginBtn').style.display = 'none';
|
||
document.getElementById('userInfo').style.display = 'block';
|
||
document.getElementById('mainApp').style.display = 'block';
|
||
document.getElementById('authRequired').style.display = 'none';
|
||
|
||
if (currentUser) {
|
||
document.getElementById('userName').textContent = currentUser.name || 'User';
|
||
document.getElementById('userEmail').textContent = currentUser.email || '';
|
||
}
|
||
|
||
// Show client info if client is selected
|
||
if (selectedClient && availableClients[selectedClient]) {
|
||
document.getElementById('clientInfo').style.display = 'block';
|
||
document.getElementById('currentClientName').textContent = availableClients[selectedClient].display_name;
|
||
} else {
|
||
document.getElementById('clientInfo').style.display = 'none';
|
||
}
|
||
|
||
// Check admin status
|
||
checkAdminStatus();
|
||
}
|
||
|
||
function showAuthRequired() {
|
||
document.getElementById('authLoading').style.display = 'none';
|
||
document.getElementById('loginBtn').style.display = 'none';
|
||
document.getElementById('userInfo').style.display = 'none';
|
||
document.getElementById('mainApp').style.display = 'none';
|
||
document.getElementById('authRequired').style.display = 'block';
|
||
}
|
||
|
||
// Protect form functionality
|
||
function requireAuthentication(callback) {
|
||
if (!isAuthenticated) {
|
||
alert('Please sign in to use this feature.');
|
||
showAuthRequired();
|
||
return false;
|
||
}
|
||
return callback();
|
||
}
|
||
|
||
// Block all form interactions until authenticated
|
||
function blockUnauthenticatedAccess(event) {
|
||
if (!isAuthenticated) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
alert('Please sign in to access this feature.');
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Initialize the application when the page loads
|
||
document.addEventListener('DOMContentLoaded', async function() {
|
||
console.log('=== DOM CONTENT LOADED EVENT FIRED ===');
|
||
console.log('Document ready state:', document.readyState);
|
||
|
||
try {
|
||
// Set up authentication event listeners
|
||
document.getElementById('loginBtn').addEventListener('click', signIn);
|
||
document.getElementById('logoutBtn').addEventListener('click', signOut);
|
||
document.getElementById('authRequiredLoginBtn').addEventListener('click', signIn);
|
||
document.getElementById('logoutFromClientSelector').addEventListener('click', signOut);
|
||
|
||
// Setup client switch handler
|
||
setupClientSwitchHandler();
|
||
|
||
// Setup admin button
|
||
document.getElementById('adminBtn').addEventListener('click', showAdminPanel);
|
||
|
||
// Load models
|
||
await loadModels();
|
||
|
||
// Check authentication status first
|
||
console.log('Checking authentication status...');
|
||
const authenticated = await checkAuthStatus();
|
||
|
||
// Only initialize app functionality if authenticated
|
||
if (authenticated) {
|
||
console.log('User authenticated, checking client selection...');
|
||
|
||
// Check if client is already selected
|
||
const storedClient = localStorage.getItem('selectedClient');
|
||
|
||
if (storedClient && availableClients[storedClient]) {
|
||
// Client already selected, load profiles and show main app
|
||
console.log('Stored client found:', storedClient);
|
||
selectedClient = storedClient;
|
||
await loadProfiles(storedClient);
|
||
await init();
|
||
updateReferenceAssetsDropdown();
|
||
updateMediaPlanDropdown();
|
||
document.getElementById('mainApp').style.display = 'block';
|
||
} else if (storedClient) {
|
||
// Client stored but not in available clients, need to load clients first
|
||
console.log('Stored client exists but need to validate, showing client selector');
|
||
showClientSelector();
|
||
} else {
|
||
// No client selected, show client selector
|
||
console.log('No stored client, showing client selector');
|
||
showClientSelector();
|
||
}
|
||
} else {
|
||
console.log('User not authenticated, showing auth required screen');
|
||
}
|
||
|
||
console.log('DOMContentLoaded handlers complete');
|
||
} catch (error) {
|
||
console.error('Error in DOMContentLoaded handler:', error);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|