ai_qc/web_ui.html
nickviljoen 564ee96ac7 Fix selectedClient persistence bug on page refresh
Issue: When refreshing the page after selecting a client, saved files
would not display because init() was called without await, causing a
race condition where loadSavedFiles() could execute before selectedClient
was properly set.

Fix: Added await to init() call in DOMContentLoaded handler (line 3280)
to ensure init() completes before continuing. This guarantees that
loadSavedFiles() is called after selectedClient is properly set.

Also added debug logging to trace selectedClient value through the
initialization process for easier troubleshooting.

Testing: After this fix, refreshing the page should properly restore
the selected client and display their saved files.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 12:15:32 +02:00

3305 lines
148 KiB
HTML
Raw 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;
}
.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;
}
</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="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;">
<div class="form-grid">
<div class="form-section">
<h3><span class="icon">📁</span>File Upload</h3>
<div class="form-group">
<label for="file-input">Select or drag your visual assets</label>
<div class="file-upload-area" id="fileUploadArea">
<input type="file" id="file-input" accept="image/*,.pdf" multiple style="display: none;">
<div style="font-size: 3em; margin-bottom: 10px;">📤</div>
<p>Click here or drag and drop your files</p>
<p style="font-size: 0.9em; color: #6c757d; margin-top: 5px;">
Supported formats: JPG, PNG, PDF, GIF, WebP<br>
Multiple files supported
</p>
</div>
<div class="file-info" id="fileInfo"></div>
<!-- Queue Display -->
<div id="fileQueue" style="margin-top: 20px; display: none;">
<h4 style="color: #495057; margin-bottom: 15px;">File Queue (<span id="queueCount">0</span> files)</h4>
<div id="queueList" style="max-height: 300px; overflow-y: auto;">
<!-- 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.15 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>
</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 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>
</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 = {};
// DOM elements (will be set in init function when DOM is ready)
let fileUploadArea, fileInput, fileInfo, profileSelect, outputMode, modelSelect, analyzeBtn, progressContainer, progressFill, currentApp, resultsContainer;
// Initialize the application
async function init() {
console.log('=== INIT FUNCTION STARTED ===');
// Initialize DOM elements now that DOM is ready
fileUploadArea = document.getElementById('fileUploadArea');
fileInput = document.getElementById('file-input');
fileInfo = document.getElementById('fileInfo');
profileSelect = document.getElementById('profile-select');
outputMode = document.getElementById('output-mode');
modelSelect = document.getElementById('model-select');
analyzeBtn = document.getElementById('analyzeBtn');
progressContainer = document.getElementById('progressContainer');
progressFill = document.getElementById('progressFill');
currentApp = document.getElementById('currentApp');
resultsContainer = document.getElementById('resultsContainer');
console.log('DOM elements initialized:', {
profileSelect: profileSelect,
fileInput: fileInput,
analyzeBtn: analyzeBtn
});
try {
console.log('About to load profiles...');
console.log('selectedClient in init():', selectedClient);
await loadProfiles(selectedClient);
console.log('Profiles loaded successfully');
console.log('About to load saved files...');
console.log('selectedClient before loadSavedFiles():', selectedClient);
await loadSavedFiles();
console.log('Saved files loaded successfully');
console.log('About to setup event listeners...');
setupEventListeners();
console.log('Event listeners setup complete');
} catch (error) {
console.error('Error in init function:', error);
}
console.log('=== INIT FUNCTION COMPLETE ===');
}
// Load available profiles from API
async function loadProfiles(clientId = null) {
try {
console.log('Loading profiles from API...');
console.log('Client ID:', clientId);
console.log('Current URL:', window.location.href);
// Build URL with optional client filter
let url = `${BASE_PATH}api/profiles`;
if (clientId) {
url += `?client=${clientId}`;
}
console.log(`Attempting to fetch: ${url}`);
const response = await fetch(url, {
credentials: 'include'
});
console.log('Response received:', response);
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Profiles API response:', data);
console.log('Response data.status:', data.status);
console.log('Status check result:', data.status === 'success');
if (data.status === 'success') {
availableProfiles = data.all_profiles;
console.log('Available profiles:', availableProfiles);
console.log('About to call populateProfileSelect()...');
console.log('populateProfileSelect function exists:', typeof populateProfileSelect);
try {
populateProfileSelect();
console.log('populateProfileSelect() called successfully');
} catch (error) {
console.error('Error calling populateProfileSelect():', error);
}
// Direct dropdown update as backup
console.log('Attempting direct dropdown update...');
const dropdown = document.getElementById('profile-select');
if (dropdown) {
console.log('Found dropdown element directly, updating...');
dropdown.innerHTML = '<option value="">Select a profile...</option>';
for (const [profileId, profileInfo] of Object.entries(availableProfiles)) {
const option = document.createElement('option');
option.value = profileId;
option.textContent = `${profileInfo.name} (${profileInfo.enabled_count} checks)`;
dropdown.appendChild(option);
}
console.log('Direct dropdown update complete with', dropdown.children.length - 1, 'profiles');
} else {
console.error('Could not find dropdown element for direct update');
}
} else {
console.error('Failed to load profiles, data.status:', data.status);
console.error('Full response data:', data);
// Show error in dropdown
profileSelect.innerHTML = '<option value="">Error loading profiles</option>';
}
} catch (error) {
console.error('Error loading profiles:', error);
console.error('Error details:', error.message, error.stack);
// Try a direct test fetch
console.log('Attempting direct health check...');
try {
const healthResponse = await fetch(`${BASE_PATH}health`);
console.log('Health check response:', healthResponse.status);
} catch (healthError) {
console.error('Health check failed:', healthError);
}
// Show error in dropdown with more details
profileSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
// Add a manual refresh option
const refreshOption = document.createElement('option');
refreshOption.value = 'refresh';
refreshOption.textContent = 'Click to retry loading profiles';
profileSelect.appendChild(refreshOption);
// Add event listener for manual retry
profileSelect.addEventListener('change', function(e) {
if (e.target.value === 'refresh') {
loadProfiles(selectedClient);
}
}, { once: true });
}
}
/**
* Load available clients from the API
*/
async function loadClients() {
try {
console.log('Loading clients from API...');
const response = await fetch(`${BASE_PATH}api/clients`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Clients API response:', data);
if (data.status === 'success') {
availableClients = data.clients;
populateClientSelector();
} else {
throw new Error(data.message || 'Failed to load clients');
}
} catch (error) {
console.error('Error loading clients:', error);
alert('Failed to load clients. Please refresh the page.');
}
}
/**
* Populate the client selector with available clients
*/
function populateClientSelector() {
const clientGrid = document.getElementById('clientGrid');
if (!clientGrid) {
console.error('Client grid element not found');
return;
}
clientGrid.innerHTML = '';
for (const [clientId, clientInfo] of Object.entries(availableClients)) {
const clientCard = document.createElement('div');
clientCard.className = 'client-card';
clientCard.dataset.clientId = clientId;
const profileCount = clientInfo.profiles.length;
const profileText = profileCount === 1 ? 'profile' : 'profiles';
clientCard.innerHTML = `
<h3>${clientInfo.display_name}</h3>
<p>${clientInfo.description}</p>
<div class="profile-count">${profileCount} ${profileText} available</div>
`;
clientCard.addEventListener('click', () => selectClient(clientId));
clientGrid.appendChild(clientCard);
}
console.log('Client selector populated with', Object.keys(availableClients).length, 'clients');
}
/**
* Handle client selection
*/
async function selectClient(clientId) {
try {
console.log('Selecting client:', clientId);
selectedClient = clientId;
// Save to localStorage for persistence
localStorage.setItem('selectedClient', clientId);
// Visual feedback
document.querySelectorAll('.client-card').forEach(card => {
card.classList.remove('selected');
});
const selectedCard = document.querySelector(`[data-client-id="${clientId}"]`);
if (selectedCard) {
selectedCard.classList.add('selected');
}
// Load profiles for this client
await loadProfiles(clientId);
// Small delay for better UX
await new Promise(resolve => setTimeout(resolve, 300));
// Hide client selector and show main app
document.getElementById('clientSelector').style.display = 'none';
document.getElementById('mainApp').style.display = 'block';
// Initialize the app
init();
updateReferenceAssetsDropdown();
// 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('clientSelector').style.display = 'block';
loadClients();
}
/**
* Setup client switch button handler
*/
function setupClientSwitchHandler() {
const switchBtn = document.getElementById('switchClientBtn');
if (switchBtn) {
switchBtn.addEventListener('click', function() {
console.log('Switch client button clicked');
// Clear current selection
selectedClient = null;
localStorage.removeItem('selectedClient');
// Show client selector
document.getElementById('mainApp').style.display = 'none';
document.getElementById('clientInfo').style.display = 'none';
showClientSelector();
});
}
}
/**
* Load available models from API
*/
async function loadModels() {
try {
console.log('Loading models from API...');
const response = await fetch(`${BASE_PATH}api/models`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Models API response:', data);
if (data.status === 'success') {
availableModels = data.models;
populateModelSelect();
// Load saved preference
const savedModel = localStorage.getItem('selectedModel');
if (savedModel && availableModels[savedModel]) {
selectModel(savedModel);
}
} else {
throw new Error(data.message || 'Failed to load models');
}
} catch (error) {
console.error('Error loading models:', error);
}
}
/**
* Populate model selection dropdown
*/
function populateModelSelect() {
// Populate both the main UI model selector and the Settings modal selector
const mainUISelect = document.getElementById('model-select');
const settingsSelect = document.getElementById('modelSelect');
// Populate main UI selector
if (mainUISelect) {
mainUISelect.innerHTML = '<option value="">Use Profile Default</option>';
for (const [modelId, modelInfo] of Object.entries(availableModels)) {
const option = document.createElement('option');
option.value = modelId;
let displayName = modelInfo.name;
if (modelInfo.status === 'beta') {
displayName += ' [BETA]';
}
option.textContent = displayName;
option.title = modelInfo.description;
mainUISelect.appendChild(option);
}
// Sync with selected model
if (selectedModel) {
mainUISelect.value = selectedModel;
}
// Add change listener to update selectedModel
mainUISelect.addEventListener('change', function() {
const modelId = this.value;
selectModel(modelId);
});
console.log('Main UI model select populated with', Object.keys(availableModels).length, 'models');
}
// Populate Settings modal selector
if (settingsSelect) {
settingsSelect.innerHTML = '<option value="">Use Profile Default</option>';
for (const [modelId, modelInfo] of Object.entries(availableModels)) {
const option = document.createElement('option');
option.value = modelId;
let displayName = modelInfo.name;
if (modelInfo.status === 'beta') {
displayName += ' [BETA]';
}
option.textContent = displayName;
option.title = modelInfo.description;
settingsSelect.appendChild(option);
}
// Add change listener for beta warning
settingsSelect.addEventListener('change', function() {
const selectedModelId = this.value;
updateBetaWarning(selectedModelId);
});
// Set current value if model is selected
if (selectedModel) {
settingsSelect.value = selectedModel;
updateBetaWarning(selectedModel);
}
console.log('Settings model select populated');
}
}
/**
* Update beta warning display
*/
function updateBetaWarning(modelId) {
const betaWarning = document.getElementById('betaWarning');
const currentDisplay = document.getElementById('currentModelDisplay');
if (!betaWarning || !currentDisplay) return;
if (modelId && availableModels[modelId]) {
const modelInfo = availableModels[modelId];
if (modelInfo.status === 'beta') {
betaWarning.style.display = 'block';
} else {
betaWarning.style.display = 'none';
}
currentDisplay.textContent = modelInfo.name;
if (modelInfo.status === 'beta') {
currentDisplay.innerHTML = modelInfo.name + ' <span style="color: #ff9800; font-weight: bold;">[BETA]</span>';
}
} else {
betaWarning.style.display = 'none';
currentDisplay.textContent = 'Use Profile Default';
}
}
/**
* Select and save model preference
*/
function selectModel(modelId) {
selectedModel = modelId;
localStorage.setItem('selectedModel', modelId);
// Update both model selectors to keep them in sync
const mainUISelect = document.getElementById('model-select');
const settingsSelect = document.getElementById('modelSelect');
if (mainUISelect) {
mainUISelect.value = modelId;
}
if (settingsSelect) {
settingsSelect.value = modelId;
updateBetaWarning(modelId);
}
console.log(`Model selected: ${modelId || 'Profile Default'}`);
}
/**
* Save model selection (called from settings modal button)
*/
function saveModelSelection() {
const modelSelect = document.getElementById('modelSelect');
if (!modelSelect) return;
const selectedModelId = modelSelect.value;
selectModel(selectedModelId);
alert(selectedModelId
? `Model preference saved: ${availableModels[selectedModelId].name}`
: 'Model preference cleared. Will use profile default.'
);
}
// Load saved output files from API with retry mechanism
async function loadSavedFiles(retryAttempts = 0, maxRetries = 3) {
try {
console.log(`Loading saved files... (attempt ${retryAttempts + 1}/${maxRetries + 1})`);
// Build URL with client filter if selected
let url = `${BASE_PATH}api/output_files`;
console.log(`DEBUG: selectedClient = '${selectedClient}'`);
if (selectedClient) {
url += `?client=${selectedClient}`;
console.log(`Filtering files by client: ${selectedClient}`);
} else {
console.log(`WARNING: No client selected, loading all files`);
}
const response = await fetch(url, {
credentials: 'include'
});
const data = await response.json();
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');
// If no files found and we're retrying, try again in a moment
if (retryAttempts < maxRetries) {
console.log('Retrying in 2 seconds...');
setTimeout(() => loadSavedFiles(retryAttempts + 1, maxRetries), 2000);
}
}
} catch (error) {
console.error('Error loading saved files:', error);
// Retry on error as well
if (retryAttempts < maxRetries) {
console.log('Retrying due to error in 2 seconds...');
setTimeout(() => loadSavedFiles(retryAttempts + 1, maxRetries), 2000);
}
}
}
// 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 = '';
files.forEach((file, index) => {
const fileDiv = document.createElement('div');
// Highlight the latest file (first in sorted list) if requested
const isLatest = index === 0 && highlightLatest;
const baseStyle = 'background: white; padding: 15px; margin: 10px 0; border-radius: 8px; border: 1px solid #dee2e6; transition: all 0.3s ease;';
const highlightStyle = isLatest ? 'background: #e8f5e8; border: 2px solid #28a745; box-shadow: 0 4px 8px rgba(40, 167, 69, 0.2);' : '';
fileDiv.style.cssText = baseStyle + highlightStyle;
const fileType = file.filename.endsWith('.html') ? 'HTML Report' : 'JSON Data';
const fileIcon = file.filename.endsWith('.html') ? '🌐' : '📄';
const fileSize = (file.size / 1024).toFixed(1) + ' KB';
const newBadge = isLatest ? '<span style="background: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; margin-left: 8px;">NEW</span>' : '';
fileDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${fileIcon} ${file.filename}${newBadge}</strong><br>
<small style="color: #6c757d;">${fileType}${fileSize} • Created: ${file.created}</small>
</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;">
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';
// Remove NEW badge
const newBadgeElement = fileDiv.querySelector('[style*="background: #28a745"]');
if (newBadgeElement) {
newBadgeElement.remove();
}
}, 5000);
}
});
savedFilesContainer.style.display = 'block';
}
// Populate profile select dropdown
function populateProfileSelect() {
console.log('FUNCTION START - populateProfileSelect');
console.log('=== POPULATE PROFILE SELECT CALLED ===');
try {
// Use fresh lookup to ensure we have the element
const profileDropdown = profileSelect || document.getElementById('profile-select');
console.log('Global profileSelect:', profileSelect);
console.log('Fresh lookup result:', document.getElementById('profile-select'));
console.log('Profile dropdown element to use:', profileDropdown);
console.log('Available profiles for population:', availableProfiles);
console.log('Available profiles keys:', Object.keys(availableProfiles || {}));
if (!profileDropdown) {
console.error('Profile select element not found!');
return;
}
profileDropdown.innerHTML = '<option value="">Select a profile...</option>';
if (!availableProfiles || Object.keys(availableProfiles).length === 0) {
console.warn('No profiles available to populate');
profileDropdown.innerHTML = '<option value="">No profiles available</option>';
return;
}
for (const [profileId, profileInfo] of Object.entries(availableProfiles)) {
console.log(`Adding profile: ${profileId}`, profileInfo);
const option = document.createElement('option');
option.value = profileId;
option.textContent = `${profileInfo.name} (${profileInfo.enabled_count} checks)`;
profileDropdown.appendChild(option);
}
console.log('Profile select populated with', profileDropdown.children.length - 1, 'profiles');
} catch (error) {
console.error('Error in populateProfileSelect:', error);
}
}
// Setup event listeners
function setupEventListeners() {
console.log('Setting up event listeners...');
console.log('Elements check:', {
fileUploadArea: !!fileUploadArea,
fileInput: !!fileInput,
profileSelect: !!profileSelect,
analyzeBtn: !!analyzeBtn,
settingsBtn: !!document.getElementById('settingsBtn')
});
// File upload area
fileUploadArea.addEventListener('click', () => fileInput.click());
fileUploadArea.addEventListener('dragover', handleDragOver);
fileUploadArea.addEventListener('dragleave', handleDragLeave);
fileUploadArea.addEventListener('drop', handleFileDrop);
// File input change
fileInput.addEventListener('change', handleFileSelect);
// Profile change
profileSelect.addEventListener('change', checkFormValidity);
// Analyze button
analyzeBtn.addEventListener('click', startAnalysis);
// Settings button
const settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) {
console.log('Adding click listener to settings button');
settingsBtn.addEventListener('click', showSettings);
} else {
console.error('Settings button not found!');
}
// Update cost display when profile changes
profileSelect.addEventListener('change', updateCostDisplay);
console.log('Event listeners setup complete');
}
// Handle drag and drop
function handleDragOver(e) {
e.preventDefault();
fileUploadArea.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
fileUploadArea.classList.remove('dragover');
}
function handleFileDrop(e) {
e.preventDefault();
fileUploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
const filesArray = Array.from(files);
if (filesArray.length === 1) {
selectedFile = filesArray[0];
displayFileInfo(filesArray[0]);
checkFormValidity();
} else {
addFilesToQueue(filesArray);
}
}
}
// Handle file selection
function handleFileSelect(e) {
const files = Array.from(e.target.files);
if (files.length > 0) {
if (files.length === 1) {
// Single file - maintain existing behavior
selectedFile = files[0];
displayFileInfo(files[0]);
checkFormValidity();
} else {
// Multiple files - add to queue
addFilesToQueue(files);
}
}
}
// Display file information
function displayFileInfo(file) {
const fileSize = (file.size / 1024 / 1024).toFixed(2);
fileInfo.innerHTML = `
<div style="background: #d4edda; padding: 15px; border-radius: 8px; border-left: 4px solid #28a745;">
<strong>Selected:</strong> ${file.name}<br>
<strong>Size:</strong> ${fileSize} MB<br>
<strong>Type:</strong> ${file.type || 'Unknown'}<br>
<button onclick="detectBrandAndType()" style="background: #17a2b8; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; margin-top: 8px; font-size: 0.9em;">🔍 Detect Brand & Type</button>
<div id="detectionResults" style="margin-top: 10px; display: none;"></div>
</div>
`;
fileInfo.style.display = 'block';
}
// Add files to queue for batch processing
function addFilesToQueue(files) {
files.forEach(file => {
fileQueue.push({
file: file,
status: 'pending',
result: null
});
});
displayQueue();
checkFormValidity();
}
// Display the file queue
function displayQueue() {
const queueContainer = document.getElementById('fileQueue');
const queueList = document.getElementById('queueList');
const queueCount = document.getElementById('queueCount');
if (fileQueue.length === 0) {
queueContainer.style.display = 'none';
return;
}
queueContainer.style.display = 'block';
queueCount.textContent = fileQueue.length;
queueList.innerHTML = fileQueue.map((item, index) => {
const fileSize = (item.file.size / 1024 / 1024).toFixed(2);
let statusColor = '#6c757d';
let statusText = 'Pending';
let statusIcon = '⏳';
if (item.status === 'analyzing') {
statusColor = '#007bff';
statusText = 'Analyzing...';
statusIcon = '🔄';
} else if (item.status === 'analyzed') {
statusColor = '#28a745';
statusText = 'Complete';
statusIcon = '✅';
} else if (item.status === 'error') {
statusColor = '#dc3545';
statusText = 'Error';
statusIcon = '❌';
}
return `
<div style="background: #f8f9fa; padding: 12px; margin-bottom: 10px; border-radius: 6px; border-left: 4px solid ${statusColor};">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<strong>${item.file.name}</strong><br>
<small style="color: #6c757d;">${fileSize} MB | ${item.file.type || 'Unknown type'}</small>
</div>
<div style="text-align: right;">
<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);
}
// 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 = `${successCount} successful, ${errorCount} errors`;
// Hide progress after delay
setTimeout(() => {
progressContainer.style.display = 'none';
}, 3000);
// Show summary
alert(`Queue processing complete!\n\n✅ Success: ${successCount}\n❌ Errors: ${errorCount}\n\nCheck the "Saved QC Files" section for results.`);
// Refresh saved files list
loadSavedFiles();
checkFormValidity();
}
// Check if form is valid and update cost
function checkFormValidity() {
// START QC ANALYSIS button - only for single file uploads
const hasSingleFile = selectedFile !== null && fileQueue.length === 0;
analyzeBtn.disabled = !(hasSingleFile && profileSelect.value);
// Process Queue button - only for pending files in queue
const processBtn = document.getElementById('processQueueBtn');
if (processBtn) {
const hasPendingFiles = fileQueue.some(item => item.status === 'pending');
processBtn.disabled = !(hasPendingFiles && profileSelect.value);
}
updateCostDisplay();
}
// Update cost display based on selected profile
function updateCostDisplay() {
const costDisplay = document.getElementById('costDisplay');
const estimatedCost = document.getElementById('estimatedCost');
const checkCount = document.getElementById('checkCount');
if (profileSelect.value && availableProfiles[profileSelect.value]) {
const profile = availableProfiles[profileSelect.value];
const numChecks = profile.enabled_count;
const cost = numChecks * 0.15;
estimatedCost.textContent = `$${cost.toFixed(2)}`;
checkCount.textContent = numChecks;
costDisplay.style.display = 'block';
} else {
costDisplay.style.display = 'none';
}
}
// Start analysis
async function startAnalysis() {
if (!selectedFile || !profileSelect.value) {
alert('Please select a file and profile before starting analysis.');
return;
}
// Show progress
progressContainer.style.display = 'block';
resultsContainer.style.display = 'none';
analyzeBtn.disabled = true;
try {
// Prepare form data
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('profile', profileSelect.value);
console.log('DEBUG: outputMode.value =', outputMode.value);
formData.append('mode', outputMode.value);
// Add model override if selected
if (selectedModel) {
formData.append('model_version', availableModels[selectedModel].model_id);
console.log('Using model override:', availableModels[selectedModel].model_id);
}
// Add selected reference asset if any
const referenceAssetSelect = document.getElementById('reference-asset-select');
if (referenceAssetSelect && referenceAssetSelect.value) {
formData.append('reference_asset', referenceAssetSelect.value);
}
// Start analysis with progress tracking
const analysisResult = await performAnalysisWithProgress(formData);
// Handle the results
await handleAnalysisResults(analysisResult);
} catch (error) {
console.error('Analysis error:', error);
showError('Analysis failed: ' + error.message);
} finally {
progressContainer.style.display = 'none';
analyzeBtn.disabled = false;
}
}
// Perform analysis with progress tracking
async function performAnalysisWithProgress(formData) {
document.getElementById('progressTitle').textContent = 'PROCESSING';
document.getElementById('progressSubtitle').textContent = 'Initializing analysis...';
currentApp.textContent = 'Starting quality control analysis...';
progressFill.style.width = '5%';
try {
console.log('Starting analysis with profile:', formData.get('profile'));
console.log('Output mode:', formData.get('mode'));
// Capture the original output mode for later use
const originalMode = formData.get('mode');
// Convert profile name to brand for the API
const profile = formData.get('profile');
if (profile && profile !== 'general') {
const brand = profile.split('_')[0];
formData.set('brand', brand);
}
// Keep the original mode selection - our async analysis supports progress tracking for both JSON and HTML modes
// Start the analysis and get session ID for progress tracking
progressFill.style.width = '10%';
currentApp.textContent = 'Submitting file for analysis...';
const response = await fetch(`${BASE_PATH}api/start_analysis`, {
method: 'POST',
body: formData,
credentials: 'include'
});
console.log('Analysis response status:', response.status);
if (!response.ok) {
throw new Error(`Analysis failed with status ${response.status}`);
}
// Check if we got a session_id for progress tracking
const responseText = await response.text();
console.log('Raw analysis response:', responseText);
let responseData;
try {
responseData = JSON.parse(responseText);
} catch (parseError) {
console.error('Failed to parse analysis response as JSON:', parseError);
console.error('Response was:', responseText);
throw new Error('Invalid JSON response from analysis endpoint');
}
console.log('Analysis response data:', responseData);
console.log('Session ID in response:', responseData.session_id);
if (responseData.session_id) {
console.log('Using real-time progress tracking for session:', responseData.session_id);
// We have a session ID, use real-time progress tracking
await trackAnalysisProgress(responseData.session_id);
// Set the original output mode back for final handling
responseData.original_mode = originalMode;
// Get final results
return responseData;
} else {
console.log('No session ID, analysis completed immediately');
// No session ID, use the response directly
progressFill.style.width = '100%';
document.getElementById('progressTitle').textContent = 'PROCESSING COMPLETE';
document.getElementById('progressSubtitle').textContent = 'Analysis complete!';
currentApp.textContent = 'Analysis complete!';
// Set the original output mode back for final handling
responseData.original_mode = originalMode;
return responseData;
}
} catch (error) {
console.error('Analysis error in performAnalysisWithProgress:', error);
throw error;
}
}
// Track analysis progress using session ID
async function trackAnalysisProgress(sessionId) {
console.log('Starting progress tracking for session:', sessionId);
return new Promise((resolve, reject) => {
const pollProgress = async () => {
try {
console.log(`Polling progress for session: ${sessionId}`);
const response = await fetch(`${BASE_PATH}api/progress/${sessionId}`, {
credentials: 'include'
});
console.log('Progress API response status:', response.status);
if (!response.ok) {
console.error(`Progress check failed with status: ${response.status}`);
const errorText = await response.text();
console.error('Error response:', errorText);
throw new Error(`Progress check failed: ${response.status}`);
}
const responseText = await response.text();
console.log('Raw progress response:', responseText);
let data;
try {
data = JSON.parse(responseText);
} catch (parseError) {
console.error('Failed to parse progress response as JSON:', parseError);
console.error('Response was:', responseText);
throw new Error('Invalid JSON response from progress endpoint');
}
console.log('Progress data received:', data);
const progress = data.progress;
console.log('Progress update:', progress);
// Update progress display
const percentage = progress.percentage || 0;
const stage = progress.stage || 'processing';
const currentCheck = progress.current_check || 'Processing';
const currentCheckDisplay = progress.current_check_display || currentCheck;
const completedChecks = progress.completed_checks || 0;
const totalChecks = progress.total_checks || 1;
// Update progress bar
progressFill.style.width = `${percentage}%`;
// Update progress text
if (stage === 'complete') {
document.getElementById('progressTitle').textContent = 'PROCESSING COMPLETE';
document.getElementById('progressSubtitle').textContent = 'Analysis complete!';
currentApp.textContent = 'All quality control checks completed successfully!';
// Return the analysis results
resolve(progress.result || data);
} else {
document.getElementById('progressTitle').textContent = 'PROCESSING';
document.getElementById('progressSubtitle').textContent = `Step ${completedChecks + 1} of ${totalChecks}`;
currentApp.textContent = `${currentCheckDisplay}`;
}
// Continue polling if not complete
if (stage !== 'complete' && stage !== 'error') {
setTimeout(pollProgress, 1000); // Poll every second
} else if (stage === 'error') {
reject(new Error(progress.error || 'Analysis failed'));
}
} catch (error) {
console.error('Progress polling error:', error);
reject(error);
}
};
// Start polling immediately
pollProgress();
});
}
// Handle analysis completion - process results and trigger download
async function handleAnalysisResults(analysisResult) {
console.log('=== HANDLING ANALYSIS RESULTS ===');
console.log('Analysis result:', analysisResult);
// Display results in the UI first
console.log('Displaying results in UI...');
displayResults(analysisResult);
// Auto-generate and download HTML report if originally requested HTML mode
if (analysisResult.status === 'success') {
if (analysisResult.original_mode === 'html' || analysisResult.session_id) {
console.log('Analysis successful, generating HTML report...');
await generateHtmlReport(analysisResult);
} else {
console.log('JSON mode requested, skipping HTML report generation');
}
} else {
console.warn('Analysis was not successful, skipping HTML report generation');
}
// Refresh saved files list with delay to ensure file is written
console.log('Refreshing saved files list...');
await refreshSavedFilesAfterAnalysis();
console.log('=== ANALYSIS RESULTS HANDLING COMPLETE ===');
}
// Generate HTML report and trigger download
async function generateHtmlReport(analysisResult) {
try {
console.log('Generating HTML report...');
console.log('Analysis result for HTML generation:', analysisResult);
// Check if the analysis result contains output file info
if (analysisResult.output_file && analysisResult.output_file.auto_saved) {
const outputFile = analysisResult.output_file;
// Show success message to user
showSuccessMessage(`HTML report saved as: ${outputFile.filename}`);
console.log('HTML report auto-saved:', outputFile.path);
} else {
console.log('No auto-save info found in analysis result, report may have been saved server-side');
showSuccessMessage('HTML report has been automatically saved to the output directory.');
}
} catch (error) {
console.error('Error with HTML report:', error);
}
}
// Generate HTML report content from analysis results
function generateHtmlReportContent(analysisResult) {
// This function is no longer needed since reports are generated server-side
return '<html><head><title>Report</title></head><body><h1>Report generated server-side</h1></body></html>';
}
// Display analysis results in the UI
function displayResults(data) {
try {
console.log('=== DISPLAY RESULTS DEBUG START ===');
console.log('Full data received:', JSON.stringify(data, null, 2));
// Validate data structure
if (!data || typeof data !== 'object') {
console.error('Invalid data structure:', data);
showError('Invalid analysis data received');
return;
}
console.log('Data validation passed');
// Extract summary information
const summary = data.summary || data.qc_analysis || {};
const qcAnalysis = data.qc_analysis || {};
const overallScore = summary.overall_score || 0;
const profileName = data.profile_selection?.suggested_profile || 'Unknown Profile';
const totalChecks = qcAnalysis.total_checks || Object.keys(qcAnalysis.check_results || {}).length;
const completedChecks = qcAnalysis.completed_checks || Object.keys(qcAnalysis.check_results || {}).length;
console.log('Extracted values:', { overallScore, profileName, totalChecks, completedChecks });
// Update results section
const resultsSection = document.getElementById('results-section');
if (resultsSection) {
resultsSection.style.display = 'block';
// Update overall score
const scoreElement = document.getElementById('overall-score');
if (scoreElement) {
scoreElement.textContent = overallScore + '/100';
scoreElement.style.color = overallScore >= 80 ? '#28a745' : overallScore >= 60 ? '#ffc107' : '#dc3545';
}
// Update grade
const gradeElement = document.getElementById('grade');
if (gradeElement) {
const grade = overallScore >= 85 ? 'Excellent' : overallScore >= 70 ? 'Good' : overallScore >= 50 ? 'Adequate' : 'Needs Improvement';
gradeElement.textContent = grade;
}
// Update checks count
const checksElement = document.getElementById('checks-count');
if (checksElement) {
checksElement.textContent = completedChecks + '/' + totalChecks;
}
// Update profile name
const profileElement = document.getElementById('profile-used');
if (profileElement) {
profileElement.textContent = profileName;
}
console.log('Results UI updated successfully');
} else {
console.warn('Results section not found in DOM');
}
// Show download button
const downloadBtn = document.getElementById('download-results');
if (downloadBtn) {
downloadBtn.style.display = 'block';
downloadBtn.onclick = () => generateHtmlReport(data, data.session_id, data.filename);
}
console.log('=== DISPLAY RESULTS DEBUG END ===');
} catch (error) {
console.error('Error displaying results:', error);
showError('Failed to display analysis results: ' + error.message);
}
}
// Show error message to user
function showError(message) {
console.error('Error:', message);
// Try to find an error display element
let errorElement = document.getElementById('error-message');
if (!errorElement) {
// Create error element if it doesn't exist
errorElement = document.createElement('div');
errorElement.id = 'error-message';
errorElement.style.cssText = 'background-color: #f8d7da; color: #721c24; padding: 15px; border: 1px solid #f5c6cb; border-radius: 8px; margin: 20px 0; font-weight: bold;';
// Insert at the top of the main container
const container = document.querySelector('.container') || document.body;
container.insertBefore(errorElement, container.firstChild);
}
errorElement.textContent = message;
errorElement.style.display = 'block';
// Hide error after 10 seconds
setTimeout(() => {
if (errorElement) {
errorElement.style.display = 'none';
}
}, 10000);
}
// Show success message to user
function showSuccessMessage(message) {
// Try to find a success display element
let successElement = document.getElementById('success-message');
if (!successElement) {
// Create success element if it doesn't exist
successElement = document.createElement('div');
successElement.id = 'success-message';
successElement.style.cssText = 'background-color: #d4edda; color: #155724; padding: 15px; border: 1px solid #c3e6cb; border-radius: 8px; margin: 20px 0; font-weight: bold;';
// Insert at the top of the main container
const container = document.querySelector('.container') || document.body;
container.insertBefore(successElement, container.firstChild);
}
successElement.textContent = message;
successElement.style.display = 'block';
// Hide success message after 8 seconds
setTimeout(() => {
if (successElement) {
successElement.style.display = 'none';
}
}, 8000);
}
// Settings functionality
function showSettings() {
console.log('Settings button clicked');
// Create modal if it doesn't exist
let modal = document.getElementById('settingsModal');
if (!modal) {
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\')">Edit Existing Profile</button>' +
'<button class="tab-btn" onclick="showTab(\'new\')">Create New Profile</button>' +
'<button class="tab-btn" onclick="showTab(\'tools\')">Tools Description</button>' +
'<button class="tab-btn" onclick="showTab(\'assets\')">Reference Assets</button>' +
'<button class="tab-btn" onclick="showTab(\'models\')">Model Selection</button>' +
'</div>' +
'<div id="existing-tab" class="tab-content active">' +
'<div style="margin-bottom: 15px;">' +
'<label>Select Profile to Edit:</label>' +
'<select id="profileSelect" onchange="loadSelectedProfile()" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc;">' +
'<option value="">Select a profile...</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>' +
'<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="tools-tab" class="tab-content">' +
'<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 tools below to create a new profile, or review what each tool does.</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 id="assets-tab" class="tab-content">' +
'<div style="margin-bottom: 20px;">' +
'<h4 style="color: #495057; margin-bottom: 10px;">Upload Brand Guidelines</h4>' +
'<p style="color: #6c757d; font-size: 0.9em; margin-bottom: 15px;">Upload brand guidelines to enhance QC analysis for your profiles.</p>' +
'</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 Brand Guidelines</p>' +
'<p style="margin: 0; color: #6c757d; font-size: 0.85em;">Add guidelines for enhanced analysis</p>' +
'</div>' +
'<form id="settingsBrandGuidelineForm" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">' +
'<div>' +
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Brand Name *</label>' +
'<input type="text" id="settingsBrandName" placeholder="Enter brand name" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 0.9em;">' +
'</div>' +
'<div>' +
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Tags (Optional)</label>' +
'<input type="text" id="settingsBrandTags" placeholder="logo, colors, typography" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; font-size: 0.9em;">' +
'</div>' +
'<div style="grid-column: 1 / -1;">' +
'<label style="display: block; margin-bottom: 5px; font-weight: 600; color: #495057; font-size: 0.9em;">Description (Optional)</label>' +
'<textarea id="settingsBrandDescription" placeholder="Brief description of this guideline file" style="width: 100%; padding: 8px; border: 2px solid #dee2e6; border-radius: 6px; height: 50px; resize: vertical; font-size: 0.9em;"></textarea>' +
'</div>' +
'</form>' +
'<div style="text-align: center;">' +
'<input type="file" id="settingsReferenceFileInput" accept=".pdf,.jpg,.png,.gif,.jpeg" 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 Guideline</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;">Existing brand guidelines will be displayed here</p>' +
'</div>' +
'</div>' +
'<div id="models-tab" class="tab-content">' +
'<div style="margin-bottom: 20px;">' +
'<h4 style="color: #495057; margin-bottom: 10px;">LLM Model Selection</h4>' +
'<p style="color: #6c757d; font-size: 0.9em; margin-bottom: 15px;">Choose which AI model to use for quality checks. Beta models may provide enhanced capabilities but could be less stable.</p>' +
'</div>' +
'<div class="form-group" style="margin-bottom: 20px;">' +
'<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #495057;">Select Model:</label>' +
'<select id="modelSelect" style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 1em;">' +
'<option value="">Use Profile Default</option>' +
'</select>' +
'</div>' +
'<div id="betaWarning" style="display: none; background: #fff3cd; border: 2px solid #ffc107; border-radius: 8px; padding: 15px; margin-top: 15px;">' +
'<strong style="color: #856404; font-size: 1.1em;">⚠️ Beta Model Selected</strong>' +
'<p style="margin: 10px 0 0 0; font-size: 0.95em; color: #856404; line-height: 1.5;">This is an experimental model and may produce unexpected results or errors. Use with caution in production environments.</p>' +
'</div>' +
'<div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">' +
'<div style="display: flex; justify-content: space-between; align-items: center;">' +
'<div>' +
'<strong style="color: #495057;">Current Selection:</strong>' +
'<span id="currentModelDisplay" style="margin-left: 10px; color: #1a73e8; font-weight: 600;">Use Profile Default</span>' +
'</div>' +
'<button onclick="saveModelSelection()" style="background: #1a73e8; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 600;">Save Model Preference</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div style="text-align: right; padding-top: 15px; border-top: 1px solid #dee2e6;">' +
'<button onclick="saveProfile()" style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; margin-right: 10px;">Save Profile</button>' +
'<button onclick="deleteProfile()" id="deleteProfileBtn" style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; margin-right: 10px; display: none;">Delete Profile</button>' +
'<button onclick="closeSettings()" style="background: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer;">Cancel</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
modal.style.cssText =
'position: fixed;' +
'top: 0;' +
'left: 0;' +
'width: 100%;' +
'height: 100%;' +
'z-index: 1000;' +
'display: none;';
document.body.appendChild(modal);
// Add modal styles
const modalStyles = document.createElement('style');
modalStyles.textContent =
'.modal-overlay {' +
'position: fixed;' +
'top: 0;' +
'left: 0;' +
'width: 100%;' +
'height: 100%;' +
'background: rgba(0,0,0,0.5);' +
'display: flex;' +
'justify-content: center;' +
'align-items: center;' +
'}' +
'.modal-content {' +
'background: white;' +
'border-radius: 8px;' +
'min-width: 600px;' +
'max-width: 800px;' +
'max-height: 80vh;' +
'overflow-y: auto;' +
'}' +
'.modal-header {' +
'display: flex;' +
'justify-content: space-between;' +
'align-items: center;' +
'padding: 20px;' +
'border-bottom: 1px solid #dee2e6;' +
'}' +
'.modal-header h2 {' +
'margin: 0;' +
'color: #333;' +
'}' +
'.close-btn {' +
'background: none;' +
'border: none;' +
'font-size: 24px;' +
'cursor: pointer;' +
'color: #666;' +
'padding: 0;' +
'width: 30px;' +
'height: 30px;' +
'display: flex;' +
'align-items: center;' +
'justify-content: center;' +
'}' +
'.close-btn:hover {' +
'color: #333;' +
'}' +
'.modal-body {' +
'padding: 20px;' +
'}' +
'.profile-tabs {' +
'display: flex;' +
'margin-bottom: 20px;' +
'border-bottom: 1px solid #dee2e6;' +
'}' +
'.tab-btn {' +
'background: none;' +
'border: none;' +
'padding: 10px 20px;' +
'cursor: pointer;' +
'border-bottom: 3px solid transparent;' +
'font-size: 14px;' +
'color: #666;' +
'}' +
'.tab-btn.active {' +
'color: #007bff;' +
'border-bottom-color: #007bff;' +
'}' +
'.tab-btn:hover {' +
'color: #007bff;' +
'}' +
'.tab-content {' +
'display: none;' +
'}' +
'.tab-content.active {' +
'display: block;' +
'}' +
'.check-item {' +
'display: grid;' +
'grid-template-columns: 1fr auto auto auto;' +
'gap: 10px;' +
'align-items: center;' +
'padding: 8px 0;' +
'border-bottom: 1px solid #f0f0f0;' +
'}' +
'.check-item:last-child {' +
'border-bottom: none;' +
'}' +
'.check-name {' +
'font-weight: 500;' +
'}' +
'.weight-input {' +
'width: 60px;' +
'padding: 4px;' +
'border: 1px solid #ccc;' +
'border-radius: 4px;' +
'text-align: center;' +
'}';
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
for (const [checkName, checkInfo] of Object.entries(currentQcApps)) {
const checkDiv = document.createElement('div');
checkDiv.className = 'check-item';
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>';
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
const deleteBtn = document.getElementById('deleteProfileBtn');
if (deleteBtn) {
deleteBtn.style.display = tabName === 'existing' ? 'inline-block' : 'none';
}
// Load brand guidelines when assets tab is shown
if (tabName === 'assets') {
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';
}
});
}
}
}
function loadSelectedProfile() {
const profileSelect = document.getElementById('profileSelect');
const selectedProfileId = profileSelect.value;
const profileEditor = document.getElementById('profileEditor');
const deleteBtn = document.getElementById('deleteProfileBtn');
if (!selectedProfileId) {
profileEditor.style.display = 'none';
deleteBtn.style.display = 'none';
return;
}
const profile = currentProfiles[selectedProfileId];
if (!profile) return;
// 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 || '';
// Update check weights
const container = document.getElementById('editQcChecksList');
const checkboxes = container.querySelectorAll('.check-enabled');
const weightInputs = container.querySelectorAll('.weight-input');
// 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 using new checks structure, extract weights
if (profile.checks && !profile.weights) {
Object.entries(profile.checks).forEach(([checkName, checkConfig]) => {
weights[checkName] = checkConfig.weight || 0;
});
}
checkboxes.forEach(checkbox => {
const checkName = checkbox.dataset.check;
checkbox.checked = enabledChecks.includes(checkName);
});
weightInputs.forEach(input => {
const checkName = input.dataset.check;
// Always multiply by 100 to convert from backend to UI (0.50 → 50.00)
input.value = (weights[checkName] || 0) * 100;
});
}
async function saveProfile() {
try {
const activeTab = document.querySelector('.tab-content.active');
const isNewProfile = activeTab.id === 'new-tab';
let profileName, profileDescription, enabledChecks, weights;
if (isNewProfile) {
profileName = document.getElementById('newProfileName').value.trim();
profileDescription = document.getElementById('newProfileDescription').value.trim();
const container = document.getElementById('newQcChecksList');
enabledChecks = [];
weights = {};
container.querySelectorAll('.check-enabled').forEach(checkbox => {
if (checkbox.checked) {
enabledChecks.push(checkbox.dataset.check);
}
});
container.querySelectorAll('.weight-input').forEach(input => {
weights[input.dataset.check] = parseFloat(input.value) || 0;
});
} else {
const profileSelect = document.getElementById('profileSelect');
const selectedProfileId = profileSelect.value;
if (!selectedProfileId) {
alert('Please select a profile to edit');
return;
}
profileName = document.getElementById('editProfileName').value.trim();
profileDescription = document.getElementById('editProfileDescription').value.trim();
const container = document.getElementById('editQcChecksList');
enabledChecks = [];
weights = {};
container.querySelectorAll('.check-enabled').forEach(checkbox => {
if (checkbox.checked) {
enabledChecks.push(checkbox.dataset.check);
}
});
container.querySelectorAll('.weight-input').forEach(input => {
weights[input.dataset.check] = parseFloat(input.value) || 0;
});
}
if (!profileName) {
alert('Please enter a profile name');
return;
}
if (enabledChecks.length === 0) {
alert('Please select at least one QC check');
return;
}
// Build checks object in the format expected by backend
// Note: Always divide by 100 to convert from UI to backend (50.00 → 0.50)
const checks = {};
for (const checkName of enabledChecks) {
checks[checkName] = {
weight: (weights[checkName] || 0) / 100,
llm: "Gemini",
enabled: true
};
}
const profileData = {
name: profileName,
description: profileDescription,
checks: checks
};
const endpoint = isNewProfile ? `${BASE_PATH}api/profiles` : `${BASE_PATH}api/profiles/${document.getElementById('profileSelect').value}`;
const method = isNewProfile ? 'POST' : 'PUT';
const response = await fetch(endpoint, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(profileData)
});
if (!response.ok) {
throw new Error(`Failed to save profile: ${response.status}`);
}
alert('Profile saved successfully!');
closeSettings();
// Refresh the profiles dropdown in the main UI
await loadProfiles();
} catch (error) {
console.error('Error saving profile:', error);
alert('Failed to save profile: ' + error.message);
}
}
async function deleteProfile() {
const profileSelect = document.getElementById('profileSelect');
const selectedProfileId = profileSelect.value;
if (!selectedProfileId) {
alert('Please select a profile to delete');
return;
}
if (!confirm('Are you sure you want to delete this profile? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`${BASE_PATH}api/profiles/${selectedProfileId}`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Failed to delete profile: ${response.status}`);
}
alert('Profile deleted successfully!');
closeSettings();
// Refresh the profiles dropdown in the main UI
await loadProfiles();
} catch (error) {
console.error('Error deleting profile:', error);
alert('Failed to delete profile: ' + error.message);
}
}
// Brand Guidelines Upload Functions (Settings Modal)
function uploadSettingsBrandGuideline() {
const fileInput = document.getElementById('settingsReferenceFileInput');
const brandName = document.getElementById('settingsBrandName').value;
const tags = document.getElementById('settingsBrandTags').value;
const description = document.getElementById('settingsBrandDescription').value;
if (!fileInput.files.length) {
showError('Please select a file to upload');
return;
}
if (!brandName.trim()) {
showError('Brand name is required');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('brand_name', brandName.trim());
formData.append('tags', tags.trim());
formData.append('description', description.trim());
// 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') {
showSuccessMessage('Brand guideline uploaded successfully!');
// Clear form
fileInput.value = '';
document.getElementById('settingsBrandName').value = '';
document.getElementById('settingsBrandTags').value = '';
document.getElementById('settingsBrandDescription').value = '';
document.getElementById('settingsSelectedFileInfo').style.display = 'none';
// Refresh brand guidelines list in settings
loadSettingsBrandGuidelines();
// Refresh main page reference assets dropdown to include new file
updateReferenceAssetsDropdown();
} 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 response = await fetch(`${BASE_PATH}api/brand_guidelines`, {
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 brand guidelines uploaded yet</p>';
return;
}
let html = '<h4 style="color: #495057; margin-bottom: 15px;">Existing Brand Guidelines</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} files)</h5>`;
brandData.guidelines.forEach(guidelineId => {
const guideline = data.files && data.files[guidelineId];
if (guideline) {
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>
<strong>${guideline.original_filename || 'Unknown file'}</strong>
${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>
<small style="color: #6c757d;">${new Date(guideline.upload_date).toLocaleDateString()}</small>
</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 brand guidelines</p>';
}
}
}
// Update reference assets dropdown to show all uploaded files
async function updateReferenceAssetsDropdown() {
const referenceAssetSelect = document.getElementById('reference-asset-select');
if (!referenceAssetSelect) return;
try {
const response = await fetch(`${BASE_PATH}api/brand_guidelines`, {
credentials: 'include'
});
const data = await response.json();
referenceAssetSelect.innerHTML = '<option value="">No reference asset selected</option>';
// Show all uploaded files regardless of brand/profile
if (data.files && Object.keys(data.files).length > 0) {
// Group files by brand for better organization
const filesByBrand = {};
for (const [fileId, fileData] of Object.entries(data.files)) {
const brandName = fileData.brand_name || 'Unknown Brand';
if (!filesByBrand[brandName]) {
filesByBrand[brandName] = [];
}
filesByBrand[brandName].push({ id: fileId, data: fileData });
}
// Add files organized by brand
for (const [brandName, files] of Object.entries(filesByBrand)) {
// Create optgroup for better organization
const optgroup = document.createElement('optgroup');
optgroup.label = brandName.charAt(0).toUpperCase() + brandName.slice(1);
files.forEach(file => {
const option = document.createElement('option');
option.value = file.id;
option.textContent = file.data.original_filename || 'Unknown file';
if (file.data.description) {
option.title = file.data.description; // Show description on hover
}
optgroup.appendChild(option);
});
referenceAssetSelect.appendChild(optgroup);
}
} else {
// No files available
referenceAssetSelect.innerHTML = '<option value="">No reference assets available</option>';
}
} catch (error) {
console.error('Error loading reference assets:', error);
referenceAssetSelect.innerHTML = '<option value="">Error loading reference assets</option>';
}
}
// MSAL Authentication Configuration
const msalConfig = {
auth: {
clientId: "9079054c-9620-4757-a256-23413042f1ef",
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
// Handle both localhost and 127.0.0.1 for local development
redirectUri: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
? 'http://localhost:7183'
: window.location.origin + window.location.pathname.replace(/\/$/, '')
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: true,
}
};
const loginRequest = {
scopes: ["openid", "profile", "email"],
prompt: "select_account"
};
// Initialize MSAL instance
let myMSALObj;
let currentUser = null;
let isAuthenticated = false;
let msalInitialized = false;
let isSigningIn = false; // Prevent concurrent sign-in attempts
try {
if (typeof msal === 'undefined') {
throw new Error('MSAL library is not available. Please check your internet connection.');
}
myMSALObj = new msal.PublicClientApplication(msalConfig);
msalInitialized = true;
console.log('MSAL initialized successfully');
} catch (error) {
console.error('Error initializing MSAL:', error);
msalInitialized = false;
}
// Authentication functions
async function signIn() {
if (!msalInitialized || !myMSALObj) {
console.error('MSAL not initialized properly');
alert('Authentication system not available. Please check your connection and try again.');
showLoginButton();
return;
}
// Prevent concurrent sign-in attempts
if (isSigningIn) {
console.log('Sign-in already in progress, ignoring duplicate request');
return;
}
try {
isSigningIn = true;
showAuthLoading();
// Clear any pending MSAL interactions
try {
localStorage.removeItem('msal.interaction.status');
sessionStorage.removeItem('msal.interaction.status');
} catch (e) {
console.warn('Could not clear MSAL storage:', e);
}
const loginResponse = await myMSALObj.loginPopup(loginRequest);
console.log('Login successful:', loginResponse);
// Send token to server for validation
const idToken = loginResponse.idToken;
const response = await fetch(`${BASE_PATH}auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: idToken })
});
const result = await response.json();
if (result.success) {
currentUser = result.user;
isAuthenticated = true;
console.log('Authentication successful:', currentUser);
// Force full page reload to fetch fresh HTML (bypasses cache)
// This ensures the client selection screen is displayed
window.location.reload(true);
} else {
throw new Error(result.error || 'Authentication failed');
}
} catch (error) {
console.error('Sign-in error:', error);
alert('Authentication failed: ' + error.message);
showLoginButton();
} finally {
isSigningIn = false; // Reset flag on both success and failure
}
}
async function signOut() {
try {
// Clear server-side session
await fetch(`${BASE_PATH}auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
// Clear client-side tokens (only if MSAL is available)
if (msalInitialized && myMSALObj) {
await myMSALObj.logoutPopup({
postLogoutRedirectUri: window.location.origin + window.location.pathname
});
}
currentUser = null;
isAuthenticated = false;
// Reload page to reset state
window.location.reload();
} catch (error) {
console.error('Sign-out error:', error);
// Even if there's an error, reset the UI
currentUser = null;
isAuthenticated = false;
window.location.reload();
}
}
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();
} else {
isAuthenticated = false;
showAuthRequired();
}
return isAuthenticated;
} catch (error) {
console.error('Auth status check failed:', error);
isAuthenticated = false;
showAuthRequired();
return false;
}
}
// 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';
}
}
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();
// 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();
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>