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>
3305 lines
148 KiB
HTML
3305 lines
148 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>AI QC - Quality Control Platform</title>
|
||
<!-- Google Fonts - Montserrat -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<!-- MSAL Browser Library v2.35.0 (last working CDN version) -->
|
||
<script src="https://alcdn.msauth.net/browser/2.35.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||
<script>
|
||
// Check if MSAL library loaded successfully
|
||
if (typeof msal === 'undefined') {
|
||
console.error('MSAL library failed to load from CDN');
|
||
// Fallback to alternative CDN
|
||
document.write('<script src="https://cdn.jsdelivr.net/npm/@azure/msal-browser@2.35.0/lib/msal-browser.min.js"><\/script>');
|
||
}
|
||
</script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #1a1a1a 0%, #000000 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 20px;
|
||
padding: 30px;
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 40px;
|
||
border-bottom: 3px solid #FFC407;
|
||
padding-bottom: 20px;
|
||
}
|
||
|
||
.header-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
.logo {
|
||
width: 120px;
|
||
height: auto;
|
||
}
|
||
|
||
.header-text p {
|
||
color: #7f8c8d;
|
||
font-size: 1.1em;
|
||
margin: 0;
|
||
margin-left: 0;
|
||
}
|
||
|
||
.settings-btn {
|
||
background: #f8f9fa;
|
||
border: 2px solid #dee2e6;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
transition: all 0.3s ease;
|
||
text-decoration: none;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.settings-btn:hover {
|
||
background: #e9ecef;
|
||
border-color: #adb5bd;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 30px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.form-section {
|
||
background: #f8f9fa;
|
||
padding: 25px;
|
||
border-radius: 15px;
|
||
border: 2px solid #e9ecef;
|
||
}
|
||
|
||
.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>
|