ai_qc/web_ui.html
nickviljoen 90563b8cf2 Add AXA document-mode QC pipeline (Phases 1, 3, 4, 5)
Multi-page PDF QC for AXA Ireland policy documents. Runs as a third mode
alongside static + video, gated on profile.mode. New code isolated under
backend/document_mode/ with new endpoints under /api/document/*.

Phase 1 — Spine + 6 deterministic doc-scope checks ($0, runs in seconds):
- Scope-aware dispatcher (document/targeted/page_sample/page_pair/page_each)
- axa_font_inventory, axa_phone_inventory, axa_bold_words_definitions,
  axa_page_numbering, axa_print_code, axa_omg_versioning
- Bootstrap bold-words dictionary extracted from Example 1 General Definitions

Phase 3 — Old-vs-new diff (~$0.50/run, 3-5 min):
- Page alignment via difflib SequenceMatcher (windowed fuzzy match)
- Vision-LLM page-pair diff via Gemini 2.5 Pro (8 concurrent)
- Two-slot upload UX, axa_policy_document_diff profile, mode=document_diff

Phase 4 — PDF accessibility (PyMuPDF, $0):
- 9 PDF/UA-1 aligned criteria (tagged structure, /MarkInfo, title, /Lang,
  encryption, font embedding, PDF version, XMP UA-conformance, alt-text)
- _run_verapdf() stub for optional Java-based veraPDF integration later

Phase 5 — Print preflight (PyMuPDF, $0):
- 7 criteria (page geometry, bleed, image colour spaces, image DPI,
  transparency, PDF/X conformance, spot colours)

Profile additions:
- axa_policy_document — 8 deterministic checks, $0 cost
- axa_policy_document_diff — 1 page-pair LLM check, ~$0.50/run

API additions:
- POST /api/document/start_analysis (single PDF)
- POST /api/document/start_diff (old + new PDFs)

Frontend additions:
- Third profile.mode value (document_diff) in applyProfileMode()
- Two-slot upload UX with PDF-only file pickers
- checkFormValidity() branches by mode for the analyse-button gate

Smoke-tested locally against Example 1 (Home Insurance V8, 86pp) and
Example 2 (Landlord V1 vs V10, 68→74pp) with real findings caught
including bold-words gaps, missing PDF/UA flag, transparency on press,
V1→V10 bold-formatting fixes. Plan + integration map + gotchas in
backend/AXA_DOCUMENT_MODE_PLAN.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:38:14 +02:00

5050 lines
242 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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,video/mp4,video/quicktime,video/x-msvideo,video/webm,.mp4,.mov,.avi,.webm,.mkv" 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>
<div id="documentModeBanner" style="display:none; margin-top:10px; padding:12px 14px; background:#eef2f7; border-left:4px solid #3a6bb1; border-radius:6px; font-size:13px; color:#243a5e;">
<strong>Document mode</strong> — multi-page PDF policy QC. Each page is rendered and scored individually. PDFs only.
</div>
<!-- Diff mode: two-slot upload (only shown when picked profile has mode=document_diff) -->
<div id="diffUploadArea" style="display:none; margin-top:14px;">
<div style="padding:12px 14px; background:#fff4d6; border-left:4px solid #b58a00; border-radius:6px; font-size:13px; color:#5a4500; margin-bottom:14px;">
<strong>Diff mode</strong> — upload the OLD and NEW versions of the same policy. Pages are aligned by text similarity, then each matched pair is diffed by Gemini.
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div>
<label for="diffOldFileInput" style="font-weight:600; display:block; margin-bottom:6px;">Old version (PDF)</label>
<div id="diffOldDrop" style="border:2px dashed #ccc; border-radius:8px; padding:16px; text-align:center; cursor:pointer; background:#fafafa;">
<input type="file" id="diffOldFileInput" accept="application/pdf,.pdf" style="display:none;">
<span id="diffOldLabel" style="color:#666;">Click to choose old PDF…</span>
</div>
</div>
<div>
<label for="diffNewFileInput" style="font-weight:600; display:block; margin-bottom:6px;">New version (PDF)</label>
<div id="diffNewDrop" style="border:2px dashed #ccc; border-radius:8px; padding:16px; text-align:center; cursor:pointer; background:#fafafa;">
<input type="file" id="diffNewFileInput" accept="application/pdf,.pdf" style="display:none;">
<span id="diffNewLabel" style="color:#666;">Click to choose new PDF…</span>
</div>
</div>
</div>
</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: #ede7f6;">
<div class="card-value" style="color: #4527a0;" id="crInputTokens">-</div>
<div class="card-label" style="color: #4527a0;">Input Tokens</div>
</div>
<div class="reporting-card" style="background: #e0f7fa;">
<div class="card-value" style="color: #00695c;" id="crOutputTokens">-</div>
<div class="card-label" style="color: #00695c;">Output Tokens</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="crProviderBreakdown" style="display: none; background: #fafafa; border: 1px solid #e9ecef; border-radius: 10px; padding: 15px 20px; margin-bottom: 15px;">
<div style="font-size: 0.85em; color: #6c757d; font-weight: 600; margin-bottom: 8px;">Cost breakdown by provider</div>
<div id="crProviderList" style="font-size: 0.9em; color: #333;"></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;">Input tok</th>
<th style="padding: 12px; text-align: right; border-bottom: 2px solid #dee2e6;">Output tok</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: 1100px; 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 and access control</p>
</div>
<button onclick="hideAdminSection()" class="settings-btn" style="background: #6c757d; color: white; border-color: #6c757d;">
Back to App
</button>
</div>
<div class="admin-tabs" style="border-bottom: 2px solid #e9ecef; margin-bottom: 25px; display: flex; gap: 8px;">
<button class="admin-tab-btn active" data-admin-tab="usage" onclick="showAdminTab('usage')" style="background: none; border: none; padding: 10px 18px; cursor: pointer; font-size: 14px; color: #007bff; border-bottom: 3px solid #007bff; margin-bottom: -2px; font-weight: 600;">
Usage Overview
</button>
<button class="admin-tab-btn" data-admin-tab="access" onclick="showAdminTab('access')" style="background: none; border: none; padding: 10px 18px; cursor: pointer; font-size: 14px; color: #666; border-bottom: 3px solid transparent; margin-bottom: -2px;">
User Access
</button>
</div>
<div id="adminUsageTab" class="admin-tab-content">
<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: #ede7f6;">
<div class="card-value" style="color: #4527a0;" id="adminTotalInputTokens">-</div>
<div class="card-label" style="color: #4527a0;">Input Tokens</div>
</div>
<div class="reporting-card" style="background: #e0f7fa;">
<div class="card-value" style="color: #00695c;" id="adminTotalOutputTokens">-</div>
<div class="card-label" style="color: #00695c;">Output Tokens</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;">Input tok</th>
<th style="text-align: right;">Output tok</th>
<th style="text-align: right;">Est. Cost</th>
</tr>
</thead>
<tbody id="adminUserTableBody">
<tr><td colspan="9" style="text-align: center; padding: 30px; color: #6c757d;">Loading users...</td></tr>
</tbody>
</table>
</div>
</div>
<div id="adminAccessTab" class="admin-tab-content" style="display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; gap: 15px;">
<input type="text" id="accessSearchInput" placeholder="Search by email or name..." oninput="renderAccessTable()" style="flex: 1; padding: 10px 14px; border: 1px solid #ced4da; border-radius: 8px; font-size: 14px;">
<button onclick="showAddAccessUser()" class="settings-btn" style="background: #007bff; color: white; border-color: #007bff; white-space: nowrap;">
+ Add User
</button>
</div>
<div id="accessDefaultBanner" style="background: #fff3cd; border: 1px solid #ffeeba; color: #856404; padding: 10px 14px; border-radius: 8px; margin-bottom: 15px; font-size: 13px;">
<strong>Default access:</strong> <span id="accessDefaultClients">general</span>. Users not listed below automatically see only these clients.
</div>
<div id="accessTableContainer" style="max-height: 520px; 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; width: 70px;">Admin</th>
<th>Clients Visible</th>
<th>Last Updated</th>
<th style="text-align: right; width: 100px;">Actions</th>
</tr>
</thead>
<tbody id="accessTableBody">
<tr><td colspan="6" style="text-align: center; padding: 30px; color: #6c757d;">Loading users...</td></tr>
</tbody>
</table>
</div>
</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);
}
// Always include the "Request Client Access" tile last so users can
// ask an admin for access to clients they currently can't see.
const requestCard = document.createElement('div');
requestCard.className = 'client-card';
requestCard.style.cssText = 'border: 2px dashed #6c757d; background: #f8f9fa;';
requestCard.innerHTML = `
<h3 style="color: #495057;">Request Client Access</h3>
<p>Need access to another client? Send a request to the admin team.</p>
<div class="profile-count" style="color: #6c757d;">Open request form →</div>
`;
requestCard.addEventListener('click', () => openAccessRequestModal());
clientGrid.appendChild(requestCard);
console.log('Client selector populated with', Object.keys(availableClients).length, 'clients');
}
async function openAccessRequestModal() {
if (!currentUser || !currentUser.email) {
alert('Please sign in first.');
return;
}
let allClients = {};
try {
const resp = await fetch(BASE_PATH + 'api/all_clients', { credentials: 'include' });
const data = await resp.json();
if (data.status === 'success') {
allClients = data.clients || {};
} else {
throw new Error(data.message || 'Could not load clients');
}
} catch (err) {
console.error('Failed to load clients for access request:', err);
alert('Could not load the client list. Please try again.');
return;
}
const accessibleIds = new Set(Object.keys(availableClients || {}));
const requestable = Object.entries(allClients).filter(([id]) => !accessibleIds.has(id));
if (requestable.length === 0) {
alert('You already have access to every client.');
return;
}
const existing = document.getElementById('accessRequestModal');
if (existing) existing.remove();
const escAttr = s => String(s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const escText = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const checklist = requestable.map(([id, info]) => `
<label style="display: flex; align-items: flex-start; padding: 10px; border: 1px solid #dee2e6; border-radius: 6px; margin-bottom: 6px; cursor: pointer;">
<input type="checkbox" value="${escAttr(id)}" style="margin-right: 10px; margin-top: 4px;">
<span><strong>${escText(info.display_name || id)}</strong><br><small style="color: #6c757d;">${escText(info.description || '')}</small></span>
</label>
`).join('');
const modal = document.createElement('div');
modal.id = 'accessRequestModal';
modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1100; display: flex; align-items: flex-start; justify-content: center; padding-top: 60px; background: rgba(0,0,0,0.45);';
modal.innerHTML = `
<div style="background: white; width: 100%; max-width: 540px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #dee2e6;">
<h2 style="margin: 0; font-size: 1.25em;">Request Client Access</h2>
<button onclick="closeAccessRequestModal()" style="background: transparent; border: none; font-size: 1.6em; cursor: pointer; line-height: 1;">&times;</button>
</div>
<div style="padding: 20px; max-height: 70vh; overflow-y: auto;">
<p style="color: #6c757d; margin-top: 0;">Your details below come from your sign-in. Pick the clients you need and we'll email the admin team.</p>
<div style="margin-bottom: 12px;">
<label style="display: block; font-weight: 600; margin-bottom: 4px;">Name</label>
<input type="text" value="${escAttr(currentUser.name || '')}" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 6px; background: #f8f9fa;">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; font-weight: 600; margin-bottom: 4px;">Email</label>
<input type="email" value="${escAttr(currentUser.email || '')}" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 6px; background: #f8f9fa;">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; font-weight: 600; margin-bottom: 4px;">Clients to request *</label>
<div id="accessRequestClientList">${checklist}</div>
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; font-weight: 600; margin-bottom: 4px;">Reason (optional)</label>
<textarea id="accessRequestReason" rows="3" placeholder="Project name, brand team, urgency, etc." style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 6px; resize: vertical;"></textarea>
</div>
<div id="accessRequestStatus" style="display: none; margin-bottom: 12px; padding: 10px; border-radius: 6px; font-size: 0.9em;"></div>
<div style="text-align: right;">
<button onclick="closeAccessRequestModal()" style="background: #6c757d; color: white; border: none; padding: 9px 18px; border-radius: 6px; cursor: pointer; margin-right: 8px;">Cancel</button>
<button id="submitAccessRequestBtn" onclick="submitAccessRequest()" style="background: #28a745; color: white; border: none; padding: 9px 18px; border-radius: 6px; cursor: pointer;">Send Request</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
}
function closeAccessRequestModal() {
const modal = document.getElementById('accessRequestModal');
if (modal) modal.remove();
}
async function submitAccessRequest() {
const checkboxes = document.querySelectorAll('#accessRequestClientList input[type="checkbox"]:checked');
const selectedClients = Array.from(checkboxes).map(cb => cb.value);
const reasonEl = document.getElementById('accessRequestReason');
const reason = reasonEl ? reasonEl.value.trim() : '';
const statusEl = document.getElementById('accessRequestStatus');
const btn = document.getElementById('submitAccessRequestBtn');
const setStatus = (kind, text) => {
if (!statusEl) return;
const palette = kind === 'error'
? { bg: '#f8d7da', fg: '#842029' }
: { bg: '#d4edda', fg: '#155724' };
statusEl.style.display = 'block';
statusEl.style.background = palette.bg;
statusEl.style.color = palette.fg;
statusEl.textContent = text;
};
if (!selectedClients.length) {
setStatus('error', 'Please select at least one client.');
return;
}
statusEl.style.display = 'none';
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const resp = await fetch(BASE_PATH + 'api/access_request', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clients: selectedClients, reason: reason })
});
const data = await resp.json();
if (resp.ok && data.status === 'success') {
setStatus('success', data.message || 'Request sent.');
btn.style.display = 'none';
setTimeout(closeAccessRequestModal, 2200);
} else {
setStatus('error', data.message || 'Could not send request.');
btn.disabled = false;
btn.textContent = 'Send Request';
}
} catch (err) {
console.error('Access request submit failed:', err);
setStatus('error', 'Network error: ' + (err.message || 'unknown'));
btn.disabled = false;
btn.textContent = 'Send Request';
}
}
/**
* 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();
}
/**
* Handle a client-access-denied response from any API call.
* Returns true if the response signalled client_access_denied (caller should stop).
*/
async function handleClientAccessDenied(response) {
if (response.status !== 403) return false;
let body;
try { body = await response.clone().json(); } catch (_) { return false; }
if (body && body.code === 'client_access_denied') {
const lost = selectedClient;
selectedClient = null;
localStorage.removeItem('selectedClient');
showAccessRevokedToast(lost);
showClientSelector();
return true;
}
return false;
}
function showAccessRevokedToast(clientId) {
let toast = document.getElementById('revokedToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'revokedToast';
toast.style.cssText = 'position: fixed; top: 25px; left: 50%; transform: translateX(-50%); background: #dc3545; color: white; padding: 14px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 10000; font-size: 14px;';
document.body.appendChild(toast);
}
toast.textContent = `Your access to "${clientId}" was removed. Please pick a different client.`;
toast.style.display = 'block';
setTimeout(() => { if (toast) toast.style.display = 'none'; }, 6000);
}
/**
* 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'
});
if (await handleClientAccessDenied(response)) {
return;
}
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') ? '&#x1F310;' : '&#x1F4C4;';
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 + ' &bull; ' + fileSize + ' &bull; 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);
profileSelect.addEventListener('change', applyProfileMode);
// 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);
// Initial mode application in case a profile is pre-selected
applyProfileMode();
// Wire up diff-mode pickers (idempotent on subsequent calls but only needs once)
wireDiffPickers();
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() {
// Diff mode: enable when BOTH diff files are picked + a profile is selected
const profileMeta = availableProfiles && availableProfiles[profileSelect.value];
const isDiffMode = !!(profileMeta && profileMeta.mode === 'document_diff');
if (isDiffMode) {
const oldInput = document.getElementById('diffOldFileInput');
const newInput = document.getElementById('diffNewFileInput');
const hasBoth = oldInput && newInput && oldInput.files.length && newInput.files.length;
analyzeBtn.disabled = !(hasBoth && profileSelect.value);
} else {
// 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();
}
// Toggle the document-mode UI based on the selected profile.
// Three modes: asset (default), document (single PDF), document_diff (two PDFs)
function applyProfileMode() {
const profileId = profileSelect ? profileSelect.value : '';
const docBanner = document.getElementById('documentModeBanner');
const diffArea = document.getElementById('diffUploadArea');
const singleFileArea = document.getElementById('fileUploadArea');
const fileInputEl = document.getElementById('file-input');
const profileMode = profileId && availableProfiles && availableProfiles[profileId]
? availableProfiles[profileId].mode
: 'asset';
const isDoc = profileMode === 'document';
const isDiff = profileMode === 'document_diff';
if (docBanner) docBanner.style.display = isDoc ? 'block' : 'none';
if (diffArea) diffArea.style.display = isDiff ? 'block' : 'none';
if (singleFileArea) singleFileArea.style.display = isDiff ? 'none' : 'block';
if (fileInputEl) {
fileInputEl.accept = isDoc
? 'application/pdf,.pdf'
: 'image/*,.pdf,video/mp4,video/quicktime,video/x-msvideo,video/webm,.mp4,.mov,.avi,.webm,.mkv';
}
}
// Wire up the diff-mode file pickers to update their labels when files are chosen.
function wireDiffPickers() {
const oldDrop = document.getElementById('diffOldDrop');
const oldInput = document.getElementById('diffOldFileInput');
const oldLabel = document.getElementById('diffOldLabel');
const newDrop = document.getElementById('diffNewDrop');
const newInput = document.getElementById('diffNewFileInput');
const newLabel = document.getElementById('diffNewLabel');
if (!oldDrop || !oldInput || !newDrop || !newInput) return;
oldDrop.addEventListener('click', () => oldInput.click());
newDrop.addEventListener('click', () => newInput.click());
oldInput.addEventListener('change', () => {
if (oldInput.files.length) {
oldLabel.textContent = oldInput.files[0].name;
oldLabel.style.color = '#222';
oldDrop.style.borderColor = '#3a6bb1';
oldDrop.style.background = '#eef2f7';
}
checkFormValidity();
});
newInput.addEventListener('change', () => {
if (newInput.files.length) {
newLabel.textContent = newInput.files[0].name;
newLabel.style.color = '#222';
newDrop.style.borderColor = '#3a6bb1';
newDrop.style.background = '#eef2f7';
}
checkFormValidity();
});
}
// 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() {
const profileMeta = availableProfiles && availableProfiles[profileSelect.value];
const isDiffMode = !!(profileMeta && profileMeta.mode === 'document_diff');
if (!profileSelect.value) {
alert('Please select a profile before starting analysis.');
return;
}
if (isDiffMode) {
const oldInput = document.getElementById('diffOldFileInput');
const newInput = document.getElementById('diffNewFileInput');
if (!oldInput || !oldInput.files.length || !newInput || !newInput.files.length) {
alert('Diff mode requires both an OLD and a NEW PDF.');
return;
}
} else if (!selectedFile) {
alert('Please select a file 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();
if (!isDiffMode) {
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 profileMeta = availableProfiles && availableProfiles[profile];
const isDocumentMode = !!(profileMeta && profileMeta.mode === 'document');
const isDiffMode = !!(profileMeta && profileMeta.mode === 'document_diff');
if ((isDocumentMode || isDiffMode) && selectedClient) {
formData.set('client_id', selectedClient);
}
// For diff mode the formData was built with a single 'file' — replace
// with old_file + new_file from the dedicated diff pickers.
if (isDiffMode) {
formData.delete('file');
const oldInput = document.getElementById('diffOldFileInput');
const newInput = document.getElementById('diffNewFileInput');
if (!oldInput || !newInput || !oldInput.files.length || !newInput.files.length) {
throw new Error('Diff mode requires both an old and a new PDF.');
}
formData.append('old_file', oldInput.files[0]);
formData.append('new_file', newInput.files[0]);
}
const analysisEndpoint = isDiffMode
? 'api/document/start_diff'
: (isDocumentMode ? 'api/document/start_analysis' : 'api/start_analysis');
const response = await fetch(`${BASE_PATH}${analysisEndpoint}`, {
method: 'POST',
body: formData,
credentials: 'include'
});
console.log('Analysis response status:', response.status);
if (await handleClientAccessDenied(response)) {
return;
}
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="margin-bottom: 15px;">' +
'<div>' +
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Name *</label>' +
'<input type="text" id="settingsBrandName" placeholder="Name shown in the reference-asset dropdown" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 0.9em;">' +
'</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="margin-bottom: 15px;">' +
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Name *</label>' +
'<input type="text" id="mediaPlanName" placeholder="Name shown in the media-plan dropdown" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 0.9em;">' +
'</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()" id="saveProfileBtn" 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()" id="cancelSettingsBtn" style="background: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; margin-right: 10px;">Cancel</button>' +
'<button onclick="closeSettings()" id="closeSettingsSaveBtn" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; display: none;">Save</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';
}
// Footer buttons: Save Profile / Cancel are for profile-editing tabs only.
// The other tabs (Reference Assets, QC Tools, Media Plan) just need a Save (close) button.
const isProfileTab = (tabName === 'existing' || tabName === 'new');
const saveProfileBtn = document.getElementById('saveProfileBtn');
const cancelSettingsBtn = document.getElementById('cancelSettingsBtn');
const closeSettingsSaveBtn = document.getElementById('closeSettingsSaveBtn');
if (saveProfileBtn) saveProfileBtn.style.display = isProfileTab ? 'inline-block' : 'none';
if (cancelSettingsBtn) cancelSettingsBtn.style.display = isProfileTab ? 'inline-block' : 'none';
if (closeSettingsSaveBtn) closeSettingsSaveBtn.style.display = isProfileTab ? 'none' : 'inline-block';
// 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' });
if (await handleClientAccessDenied(response)) {
return;
}
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(', ');
const heading = plan.display_name || plan.original_filename;
const filenameLine = plan.display_name ? ('<small style="color: #6c757d;">File: ' + plan.original_filename + '</small><br>') : '';
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;">' + heading + '</span><br>' +
filenameLine +
'<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 nameInput = document.getElementById('mediaPlanName');
const planName = nameInput ? nameInput.value.trim() : '';
if (!planName) {
showError('Please enter a name for this media plan');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('client_id', selectedClient || 'general');
formData.append('display_name', planName);
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 = '';
if (nameInput) nameInput.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;
if (!fileInput.files.length) {
showError('Please select a file to upload');
return;
}
if (!brandName.trim()) {
showError('Please enter a name for this reference asset');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('brand_name', brandName.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('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('crInputTokens').textContent = formatTokenCount(data.total_input_tokens || 0);
document.getElementById('crOutputTokens').textContent = formatTokenCount(data.total_output_tokens || 0);
document.getElementById('crEstCost').textContent = '$' + data.estimated_cost_usd.toFixed(2);
cards.style.display = 'grid';
// Provider breakdown
const providerEl = document.getElementById('crProviderBreakdown');
const providerList = document.getElementById('crProviderList');
const byProvider = data.by_provider || {};
const providerKeys = Object.keys(byProvider);
if (providerKeys.length > 0) {
providerList.innerHTML = providerKeys.map(p => {
const s = byProvider[p];
return '<div style="margin: 4px 0;">' +
'<strong>' + escapeHtml(p) + ':</strong> ' +
formatTokenCount(s.input_tokens || 0) + ' in / ' +
formatTokenCount(s.output_tokens || 0) + ' out — ' +
'<span style="color: #c62828;">$' + Number(s.cost_usd || 0).toFixed(4) + '</span>' +
'</div>';
}).join('');
providerEl.style.display = 'block';
} else {
providerEl.style.display = 'none';
}
// 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;">' + formatTokenCount(r.input_tokens || 0) + '</td>' +
'<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: right;">' + formatTokenCount(r.output_tokens || 0) + '</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';
showAdminTab('usage');
}
function showAdminTab(tab) {
document.querySelectorAll('.admin-tab-btn').forEach(btn => {
const active = btn.dataset.adminTab === tab;
btn.classList.toggle('active', active);
btn.style.color = active ? '#007bff' : '#666';
btn.style.borderBottomColor = active ? '#007bff' : 'transparent';
btn.style.fontWeight = active ? '600' : 'normal';
});
document.getElementById('adminUsageTab').style.display = tab === 'usage' ? 'block' : 'none';
document.getElementById('adminAccessTab').style.display = tab === 'access' ? 'block' : 'none';
if (tab === 'usage') {
loadAdminUsers();
} else if (tab === 'access') {
loadAccessData();
}
}
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('adminTotalInputTokens').textContent = formatTokenCount(data.total_platform_input_tokens || 0);
document.getElementById('adminTotalOutputTokens').textContent = formatTokenCount(data.total_platform_output_tokens || 0);
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;">' + formatTokenCount(u.input_tokens || 0) + '</td>' +
'<td style="text-align: right;">' + formatTokenCount(u.output_tokens || 0) + '</td>' +
'<td style="text-align: right;">$' + u.total_cost.toFixed(2) + '</td>' +
'</tr>';
}).join('');
} else {
tbody.innerHTML = '<tr><td colspan="9" 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="9" style="text-align: center; padding: 30px; color: #dc3545;">Error: ' + error.message + '</td></tr>';
}
}
// ===== User Access Control =====
let accessData = { default_clients: [], users: [] };
let allClientsForAccess = {};
async function loadAccessData() {
try {
const [accessResp, clientsResp] = await Promise.all([
fetch(`${BASE_PATH}api/admin/user_access`, { credentials: 'include' }),
fetch(`${BASE_PATH}api/clients`, { credentials: 'include' })
]);
const accessJson = await accessResp.json();
const clientsJson = await clientsResp.json();
if (accessJson.status !== 'success') throw new Error(accessJson.message || 'Failed to load access data');
accessData = { default_clients: accessJson.default_clients || [], users: accessJson.users || [] };
// Admin users see all clients from /api/clients — use that as the full catalogue
allClientsForAccess = clientsJson.clients || {};
document.getElementById('accessDefaultClients').textContent =
accessData.default_clients.length ? accessData.default_clients.join(', ') : '(none)';
renderAccessTable();
} catch (error) {
console.error('Error loading access data:', error);
document.getElementById('accessTableBody').innerHTML =
'<tr><td colspan="6" style="text-align: center; padding: 30px; color: #dc3545;">Error: ' + error.message + '</td></tr>';
}
}
function renderAccessTable() {
const tbody = document.getElementById('accessTableBody');
const query = (document.getElementById('accessSearchInput').value || '').toLowerCase().trim();
const filtered = accessData.users.filter(u => {
if (!query) return true;
return (u.email || '').toLowerCase().includes(query) ||
(u.name || '').toLowerCase().includes(query);
});
if (!filtered.length) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 30px; color: #6c757d;">No users match</td></tr>';
return;
}
tbody.innerHTML = filtered.map(u => {
const clientsText = u.is_admin
? '<em style="color: #6c757d;">all clients (admin)</em>'
: (u.clients && u.clients.length ? u.clients.join(', ') : '<em style="color: #6c757d;">none</em>');
const updated = u.updated_at
? new Date(u.updated_at).toLocaleDateString() + (u.updated_by ? ` by ${u.updated_by}` : '')
: '—';
const safeEmail = (u.email || '').replace(/'/g, "\\'");
return `
<tr data-access-row="${u.email}">
<td>${escapeHtml(u.name || '')}</td>
<td>${escapeHtml(u.email || '')}</td>
<td style="text-align: center;">${u.is_admin ? '🔑' : ''}</td>
<td>${clientsText}</td>
<td style="color: #6c757d; font-size: 12px;">${escapeHtml(updated)}</td>
<td style="text-align: right;">
<button onclick="openAccessEditor('${safeEmail}')" class="settings-btn" style="padding: 4px 10px; font-size: 12px;">Edit</button>
</td>
</tr>`;
}).join('');
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function formatTokenCount(n) {
const v = Number(n) || 0;
if (v >= 1_000_000) return (v / 1_000_000).toFixed(2) + 'M';
if (v >= 1_000) return (v / 1_000).toFixed(1) + 'K';
return v.toLocaleString();
}
function openAccessEditor(email) {
const user = accessData.users.find(u => (u.email || '').toLowerCase() === email.toLowerCase());
if (!user) return;
const row = document.querySelector(`tr[data-access-row="${user.email}"]`);
if (!row) return;
// Collapse any other open editor
document.querySelectorAll('tr.access-editor-row').forEach(r => r.remove());
const clientCheckboxes = Object.entries(allClientsForAccess).map(([id, info]) => {
const checked = user.is_admin || (user.clients || []).includes(id);
const disabled = user.is_admin;
return `<label style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: #f8f9fa; border-radius: 6px; cursor: ${disabled ? 'not-allowed' : 'pointer'}; opacity: ${disabled ? 0.6 : 1};">
<input type="checkbox" data-client-id="${id}" ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
${escapeHtml(info.display_name || info.name || id)}
</label>`;
}).join('');
const editorRow = document.createElement('tr');
editorRow.className = 'access-editor-row';
editorRow.innerHTML = `
<td colspan="6" style="background: #f8f9fa; padding: 18px;">
<div style="display: flex; flex-direction: column; gap: 14px;">
<div style="font-weight: 600;">Editing access for ${escapeHtml(user.email)}</div>
<div>
<div style="font-size: 13px; color: #495057; margin-bottom: 8px;">Clients visible to this user:</div>
<div id="accessEditorClients" style="display: flex; flex-wrap: wrap; gap: 8px;">${clientCheckboxes}</div>
${user.is_admin ? '<div style="font-size: 12px; color: #6c757d; margin-top: 8px;">Admins always see all clients. Demote to admin to edit client list.</div>' : ''}
</div>
<div>
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="accessEditorAdmin" ${user.is_admin ? 'checked' : ''}>
Grant admin access (sees all clients)
</label>
</div>
<div id="accessEditorError" style="color: #dc3545; font-size: 13px; display: none;"></div>
<div style="display: flex; justify-content: flex-end; gap: 10px;">
<button onclick="closeAccessEditor()" class="settings-btn" style="background: #fff; color: #495057;">Cancel</button>
<button onclick="saveAccessEditor('${user.email.replace(/'/g, "\\'")}')" class="settings-btn" style="background: #28a745; color: white; border-color: #28a745;">Save</button>
</div>
</div>
</td>`;
row.after(editorRow);
}
function closeAccessEditor() {
document.querySelectorAll('tr.access-editor-row').forEach(r => r.remove());
// If the editor was opened for an unsaved "Add User" row, drop it from the local list
const hadUnsaved = accessData.users.some(u => u._unsaved);
if (hadUnsaved) {
accessData.users = accessData.users.filter(u => !u._unsaved);
renderAccessTable();
}
}
async function saveAccessEditor(email) {
const errorEl = document.getElementById('accessEditorError');
errorEl.style.display = 'none';
const currentUser = accessData.users.find(u => (u.email || '').toLowerCase() === email.toLowerCase());
if (!currentUser) return;
const wantsAdmin = document.getElementById('accessEditorAdmin').checked;
const clients = Array.from(document.querySelectorAll('#accessEditorClients input[type=checkbox]'))
.filter(cb => cb.checked && !cb.disabled)
.map(cb => cb.dataset.clientId);
try {
// Promote / demote first if admin state changed
if (wantsAdmin !== currentUser.is_admin) {
const action = wantsAdmin ? 'promote' : 'demote';
const resp = await fetch(`${BASE_PATH}api/admin/user_access/${encodeURIComponent(email)}/${action}`, {
method: 'POST',
credentials: 'include'
});
const body = await resp.json();
if (body.status !== 'success') throw new Error(body.message || 'Failed to change admin role');
}
// Set client list (skip if user is/will be admin — admins see all regardless)
if (!wantsAdmin) {
const resp = await fetch(`${BASE_PATH}api/admin/user_access/${encodeURIComponent(email)}`, {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clients })
});
const body = await resp.json();
if (body.status !== 'success') throw new Error(body.message || 'Failed to save access');
}
showAccessToast(`Saved access for ${email}`);
closeAccessEditor();
await loadAccessData();
} catch (err) {
errorEl.textContent = err.message;
errorEl.style.display = 'block';
}
}
function showAddAccessUser() {
const email = prompt('Enter the email address of the user to grant access to:');
if (!email || !email.includes('@')) return;
const trimmed = email.trim().toLowerCase();
// If already in the list, just open their editor
const existing = accessData.users.find(u => (u.email || '').toLowerCase() === trimmed);
if (existing) {
openAccessEditor(existing.email);
return;
}
// Add to the local list (not persisted until they hit Save) and open editor
accessData.users.unshift({
email: trimmed,
name: '',
clients: accessData.default_clients,
is_admin: false,
updated_at: null,
updated_by: null,
has_explicit_grant: false,
last_active: '',
_unsaved: true
});
renderAccessTable();
openAccessEditor(trimmed);
}
function showAccessToast(message) {
let toast = document.getElementById('accessToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'accessToast';
toast.style.cssText = 'position: fixed; bottom: 25px; right: 25px; background: #28a745; color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 9999; font-size: 14px; opacity: 0; transition: opacity 0.3s;';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.style.opacity = '1';
clearTimeout(toast._hideTimer);
toast._hideTimer = setTimeout(() => { toast.style.opacity = '0'; }, 3000);
}
// 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) {
for (const [fileId, fileData] of Object.entries(data.files)) {
const option = document.createElement('option');
option.value = fileId;
// Prefer the user-entered name, fall back to original filename for legacy records
option.textContent = fileData.brand_name || fileData.original_filename || 'Unnamed asset';
if (fileData.original_filename) {
option.title = fileData.original_filename;
}
referenceAssetSelect.appendChild(option);
}
} 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(', ');
const planLabel = plan.display_name || plan.original_filename;
mediaPlanSelect.innerHTML =
'<option value="">No media plan selected</option>' +
'<option value="use" selected>' + planLabel + ' (' + 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>';
}
}
// Build the MSAL redirect URI to match what's registered in Azure AD.
// Both dev and prod are registered with the trailing-slash form, so we
// preserve the pathname as-is (the deployed URL always ends with "/ai_qc/").
function buildMsalRedirectUri() {
const host = window.location.hostname;
if (host === 'localhost' || host === '127.0.0.1') {
return 'http://localhost:7183';
}
return window.location.origin + window.location.pathname;
}
// MSAL Authentication Configuration
const msalConfig = {
auth: {
clientId: "9079054c-9620-4757-a256-23413042f1ef",
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
redirectUri: buildMsalRedirectUri()
},
cache: {
cacheLocation: "localStorage",
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: buildMsalRedirectUri()
});
}
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 token refresh - uses MSAL to get a fresh token without user interaction
async function silentTokenRefresh() {
if (!msalInitialized || !myMSALObj) {
console.warn('MSAL not available for silent refresh');
return false;
}
try {
const accounts = myMSALObj.getAllAccounts();
if (accounts.length === 0) {
console.warn('No MSAL accounts found for silent refresh');
return false;
}
const silentRequest = {
scopes: ["openid", "profile", "email"],
account: accounts[0],
forceRefresh: true
};
const tokenResponse = await myMSALObj.acquireTokenSilent(silentRequest);
console.log('Silent token refresh successful');
// Send the fresh token to the backend to update the cookie
const response = await fetch(`${BASE_PATH}auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: tokenResponse.idToken })
});
const result = await response.json();
if (result.success) {
console.log('Session refreshed successfully');
return true;
} else {
console.warn('Backend rejected refreshed token:', result.error);
return false;
}
} catch (error) {
console.warn('Silent token refresh failed:', error.message);
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 - attempting silent token refresh...');
// Try to silently refresh the token before prompting re-login
const refreshed = await silentTokenRefresh();
if (refreshed) {
console.log('Session restored via silent refresh');
return; // Session restored, no need to prompt
}
// Silent refresh failed - user must re-authenticate
console.warn('Silent refresh failed - 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
}
}
// Proactive token refresh - runs on a longer interval to keep session alive
async function proactiveTokenRefresh() {
if (!isAuthenticated) return;
try {
const refreshed = await silentTokenRefresh();
if (refreshed) {
console.log('Proactive token refresh completed');
}
} catch (error) {
console.warn('Proactive token refresh failed (will retry next interval):', error.message);
}
}
let proactiveRefreshInterval = null;
function startPeriodicAuthCheck() {
if (authCheckInterval) return; // Already running
// Check auth status every 5 minutes (300000ms)
authCheckInterval = setInterval(silentAuthCheck, 5 * 60 * 1000);
// Proactively refresh token every 45 minutes to prevent expiry
proactiveRefreshInterval = setInterval(proactiveTokenRefresh, 45 * 60 * 1000);
console.log('Periodic auth check started (status every 5 min, token refresh every 45 min)');
}
function stopPeriodicAuthCheck() {
if (authCheckInterval) {
clearInterval(authCheckInterval);
authCheckInterval = null;
}
if (proactiveRefreshInterval) {
clearInterval(proactiveRefreshInterval);
proactiveRefreshInterval = 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>