// ========================== // == GLOBAL CONFIGURATION == // ========================== const API_BASE_URL = ""; // Variables to store session/run state let runId; let imageKey; let upload; // --- Grid overlay state --- let viewGridEnabled = false; const GRID_ROWS = 7; // ViT-B/32 → 7×7 patch grid const GRID_COLS = 7; // keep rows == cols const CELL_SIM_K = 25; // --- Available models list --- let availableModels = []; let selectedModel = ''; let creatorsMap = {}; let selectedCreators = []; // --- Cell highlight state --- let cellHighlightTimeout = null; function updateCreatorTags() { const tagContainer = $('#creatorTags'); tagContainer.empty(); selectedCreators.forEach(name => { const tag = $('') .addClass('badge bg-primary px-3 py-2 d-flex align-items-center') .html(`${name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} `); tag.find('i').on('click', function () { selectedCreators = selectedCreators.filter(c => c !== name); updateCreatorTags(); }); tagContainer.append(tag); }); } /** * Appends a message to the working log with the specified type. * @param {string} message - The message to display. * @param {string} [type='text-white'] - The CSS class for the message (e.g., 'text-white', 'text-danger'). */ function logWorkingMessage(message, type = 'text-white') { const logContainer = $('#workingLog'); if (!logContainer.length) { // overlay now has no log box console.log('[WORKING]', message); // fallback for dev console return; } logContainer.append(`
${message}
`); logContainer.scrollTop(logContainer[0].scrollHeight); } // ========================== // == TOPIC TAG SELECTION == // ========================== let selectedTopics = []; let topicMap = {}; /** * Updates the display of selected topics in the #selectedTopicsWrapper. * Shows all topics from topicMap, visually indicating which are selected. */ function updateSelectedTopicsDisplay() { $('#selectedTopicsWrapper').removeClass('d-none'); const selectedTagContainer = $('#selectedTopicTags'); selectedTagContainer.empty(); for (const [code, label] of Object.entries(topicMap)) { const isSelected = selectedTopics.includes(code); const tag = $(' `); $('#uploadedImageContainer').prepend(uploadCard); } // Show example container if it was hidden showLandingContent(); // ← NEW adjustMainWidth(); }); // --- Load topic tags from /topics --- fetch(`${API_BASE_URL}/topics`) .then(response => response.json()) .then(data => { topicMap = data; const tagContainer = document.getElementById('topicTags'); for (const [code, label] of Object.entries(data)) { const tag = document.createElement('button'); tag.className = 'btn btn-outline-primary btn-sm px-3 py-1 rounded-pill'; tag.textContent = label; tag.dataset.code = code; tag.addEventListener('click', function () { this.classList.toggle('active'); if (this.classList.contains('active')) { this.classList.replace('btn-outline-primary', 'btn-primary'); selectedTopics.push(code); } else { this.classList.replace('btn-primary', 'btn-outline-primary'); selectedTopics = selectedTopics.filter(c => c !== code); } }); tagContainer.appendChild(tag); } }) .catch(error => { console.error('Error loading topics:', error); }); // --- Load model list from /models --- fetch(`${API_BASE_URL}/models`) .then(response => response.json()) .then(data => { availableModels = data; console.log("Available models:", availableModels); // Populate the model dropdown const dropdownMenu = $('#modelDropdownMenu'); if (availableModels.length > 0) { dropdownMenu.empty(); availableModels.forEach((model, index) => { const item = $('
  • ' + model + '
  • '); if (index === 0) { $('#modelDropdown').text('AI Model: ' + model); item.find('a').addClass('active'); selectedModel = model; } item.on('click', function () { selectedModel = model; $('#modelDropdownMenu a').removeClass('active'); $(this).find('a').addClass('active'); $('#modelDropdown').text('AI Model: ' + model); }); dropdownMenu.append(item); }); } }) .catch(error => { console.error('Error loading models:', error); }); // --- Load creators list from /creators --- fetch(`${API_BASE_URL}/creators`) .then(response => response.json()) .then(data => { creatorsMap = data; console.log("Available creators:", creatorsMap); }) .catch(error => { console.error('Error loading creators:', error); }); // --- Creator search logic --- $('#creatorSearch').on('input', function () { const query = $(this).val().toLowerCase(); const resultsContainer = $('#creatorSearchResults'); resultsContainer.empty(); if (query.length > 0) { const matches = Object.keys(creatorsMap).filter(name => name.toLowerCase().includes(query) ); matches.forEach((name) => { const item = $(' `); li.find('span').on('click', function () { lookupDOI(li.data('work'), li.data('sentence')); }); li.find('.heatmap-btn').on('click', function(e) { e.stopPropagation(); requestHeatmap($(this).data('sentence')); }); $('#sentenceList').append(li); }); showBottomCards(); adjustMainWidth(); } catch (err) { console.error('Error in display_sentences:', err); logWorkingMessage('Error displaying sentences: ' + err.message, 'text-danger'); $('#workingOverlay').addClass('d-none'); } } // helper runs whenever the right-hand column is shown/hidden function adjustMainWidth(){ const $main = $('#uploadedImageContainer').closest('.col-md-9'); if ($('.col-md-3').hasClass('d-none')){ $main.addClass('fill'); } else{ $main.removeClass('fill'); } } // ────────────────────────────────────────────────────────────────────────────── // NEW helper : always reveal bottom-row cards // ────────────────────────────────────────────────────────────────────────────── function showBottomCards() { $('#imageHistoryWrapper').removeClass('d-none'); $('#selectedTopicsWrapper').removeClass('d-none'); $('#selectedCreatorsWrapper').removeClass('d-none'); } // ────────────────────────────────────────────────────────────────────────────── // NEW helper : save current image to history // ────────────────────────────────────────────────────────────────────────────── function saveCurrentImageToHistory() { const currentImg = $('#uploadedImage'); if (!currentImg.attr('src') || currentImg.hasClass('d-none')) { return; // No image to save } // Check if this image is already the most recent in history const firstHistoryImg = $('#imageHistory img').first(); if (firstHistoryImg.length && firstHistoryImg.attr('src') === currentImg.attr('src')) { return; // Don't duplicate the same image } const historyImg = new Image(); historyImg.src = currentImg.attr('src'); historyImg.className = "rounded border border-secondary shadow-sm"; historyImg.style.height = "100px"; historyImg.style.cursor = "pointer"; historyImg.title = "Previous image"; $('#imageHistoryWrapper').removeClass('d-none'); $('#imageHistory').prepend(historyImg); } // --- Begin Crop Tool Functionality --- // Variables for cropping state let isCropping = false; let cropStartX = 0; let cropStartY = 0; let cropRect = null; // Activate cropping mode when crop tool button is clicked $('#cropToolBtn').on('click', function () { isCropping = true; $('#uploadedImageContainer').css('cursor', 'crosshair'); }); // Start drawing crop rectangle on mouse down $('#uploadedImageContainer').on('mousedown', function (e) { if (!isCropping) return; const rect = this.getBoundingClientRect(); cropStartX = e.clientX - rect.left; cropStartY = e.clientY - rect.top; if (cropRect) { cropRect.remove(); } cropRect = $('
    ') .addClass('position-absolute border border-warning') .css({ left: cropStartX, top: cropStartY, width: 0, height: 0, zIndex: 10, pointerEvents: 'none' }) .appendTo('#uploadedImageContainer'); }); // Update crop rectangle size on mouse move $('#uploadedImageContainer').on('mousemove', function (e) { if (!isCropping || !cropRect) return; const rect = this.getBoundingClientRect(); const currentX = e.clientX - rect.left; const currentY = e.clientY - rect.top; const width = Math.abs(currentX - cropStartX); const height = Math.abs(currentY - cropStartY); const left = Math.min(currentX, cropStartX); const top = Math.min(currentY, cropStartY); cropRect.css({ left, top, width, height }); }); // Complete cropping on mouse up, update image, and save history $('#uploadedImageContainer').on('mouseup', function (e) { if (!isCropping || !cropRect) return; isCropping = false; $('#uploadedImageContainer').css('cursor', 'default'); const img = document.getElementById('uploadedImage'); // Use the actual image's bounding box for accurate alignment const imageRect = img.getBoundingClientRect(); const cropOffset = cropRect.offset(); // Calculate crop rectangle relative to image's natural size const sx = ((cropOffset.left - imageRect.left) / imageRect.width) * img.naturalWidth; const sy = ((cropOffset.top - imageRect.top) / imageRect.height) * img.naturalHeight; const sw = (cropRect.width() / imageRect.width) * img.naturalWidth; const sh = (cropRect.height() / imageRect.height) * img.naturalHeight; // Don't crop if width or height is zero or negative if (sw <= 0 || sh <= 0) { cropRect.remove(); cropRect = null; return; } // REMOVED: Save current image to history using the new helper // REMOVED: saveCurrentImageToHistory(); // Draw the cropped region onto a canvas and update the image const canvas = document.createElement('canvas'); canvas.width = sw; canvas.height = sh; const ctx = canvas.getContext('2d'); ctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh); img.src = canvas.toDataURL(); $('#uploadedImage').removeClass('d-none'); cropRect.remove(); cropRect = null; $('#workingOverlay').removeClass('d-none'); logWorkingMessage('Rerunning with cropped image...', 'text-white'); fetchPresign(); }); // --- End Crop Tool Functionality --- // --- Begin Undo Tool Functionality --- // Restore previous image from history when undo is clicked $('#undoToolBtn').on('click', function () { const historyImgs = $('#imageHistory img'); if (historyImgs.length > 0) { const firstImg = historyImgs.first(); const previousSrc = firstImg.attr('src'); $('#uploadedImage').attr('src', previousSrc).removeClass('d-none'); firstImg.remove(); } }); // --- End Undo Tool Functionality --- // --- Begin Image History Selection Functionality --- // When a history image is clicked, make it the current image and rerun the API flow $('#imageHistory').on('click', 'img', function () { const currentImg = $('#uploadedImage')[0]; const newSrc = $(this).attr('src'); // REMOVED: Only save to history if it's a different image // REMOVED: if (currentImg.src && currentImg.src !== newSrc) { // REMOVED: saveCurrentImageToHistory(); // REMOVED: } // Update to the selected history image $('#uploadedImage').attr('src', newSrc).removeClass('d-none'); // Rerun the API processing $('#workingOverlay').removeClass('d-none'); logWorkingMessage('Rerunning with selected image from history...', 'text-white'); fetchPresign(); }); // --- End Image History Selection Functionality --- // --- Begin Rerun Tool Functionality --- // Rerun the backend pipeline with the current image $('#rerunToolBtn').on('click', function () { $('#workingOverlay').removeClass('d-none'); logWorkingMessage('Rerunning with current image...', 'text-white'); fetchPresign(); }); // --- End Rerun Tool Functionality --- // NEW: Grid toggle button handler $('#gridToolBtn').on('click', function () { viewGridEnabled = !viewGridEnabled; updateGridVisibility(); $(this).toggleClass('active'); // visual feedback }); /** * Looks up metadata for a given work ID (e.g., DOI) and displays details. * @param {string} work_id - The identifier for the work to look up. */ function lookupDOI(work_id, sentence) { const url = `${API_BASE_URL}/work/${encodeURIComponent(work_id)}` + `?sentence=${encodeURIComponent(sentence)}`; fetch(url) .then(res => res.json()) .then(data => { data.Work_ID = work_id; showWorkDetails(data); // now contains .context }) .catch(console.error); } /** * Displays work/DOI details in a centred, scrollable modal rectangle with a dimmed backdrop. * @param {Object} workData */ function showWorkDetails(workData) { // ――― Clean-up any prior overlay ――― $('#workOverlayBackdrop, #workDetailsModal').remove(); const d = workData; const ctxHtml = d.context ? `

    In Context

    ${escapeHTML(d.context)}
    ` : ''; /* ---------- backdrop + centred rectangle ---------- */ const backdrop = $( // ← MISSING, caused ReferenceError `
    ` ); const modal = $(`
    ${d.Work_Title || 'Unknown Title'}

    Author(s): ${d.Author_Name || 'Unknown Author'}

    Year: ${d.Year || 'Unknown'}

    ${ctxHtml}
    Images in this work

    DOI: ${d.DOI}

    Download Link: ${d.Link}

    BibTeX Citation
    ${d.BibTeX || 'Citation not available'}
            
    `); // inject into DOM $('body').append(backdrop, modal); /* ---------- gallery fetch ---------- */ if (d.Work_ID) { fetch(`${API_BASE_URL}/images/${d.Work_ID}`) .then(r => r.json()) .then(urls => { if (!urls.length) { $('#galleryWrapper').hide(); return; } const scroller = $('#galleryScroller'); urls.forEach(u => $('') .attr('src', u) .attr('crossorigin', 'anonymous') // ensure CORS safe for canvas .addClass('img-thumbnail') .css({ height: '120px', cursor: 'pointer' }) .on('click', () => loadImageAndRun(u)) .appendTo(scroller)); }) .catch(console.error); } /* ---------- close handlers ---------- */ backdrop.on('click', () => { backdrop.remove(); modal.remove(); }); modal.on('click', '#workDetailsClose', () => { backdrop.remove(); modal.remove(); }); } // Add this helper function for copying BibTeX function copyBibTeX() { const bibtexText = document.getElementById('bibtexContent').textContent.trim(); // Create a temporary textarea to copy from const tempTextarea = document.createElement('textarea'); tempTextarea.value = bibtexText; tempTextarea.style.position = 'fixed'; tempTextarea.style.opacity = '0'; document.body.appendChild(tempTextarea); // Select and copy the text tempTextarea.select(); document.execCommand('copy'); document.body.removeChild(tempTextarea); // Visual feedback - change icon temporarily const copyBtn = event.target.closest('button'); const icon = copyBtn.querySelector('i'); icon.classList.remove('bi-clipboard'); icon.classList.add('bi-clipboard-check'); // Change button text temporarily copyBtn.setAttribute('title', 'Copied!'); // Reset after 2 seconds setTimeout(() => { icon.classList.remove('bi-clipboard-check'); icon.classList.add('bi-clipboard'); copyBtn.setAttribute('title', 'Copy to clipboard'); }, 2000); } /** * Positions the #gridOverlay to exactly cover the visible image area. */ function positionGridOverlayToImage() { const container = document.getElementById('uploadedImageContainer'); const img = document.getElementById('uploadedImage'); const overlay = document.getElementById('gridOverlay'); if (!container || !img || !overlay) return; if (img.classList.contains('d-none') || !img.src) return; const containerRect = container.getBoundingClientRect(); const imageRect = img.getBoundingClientRect(); // Compute image rect relative to the container const left = imageRect.left - containerRect.left; const top = imageRect.top - containerRect.top; overlay.style.left = `${left}px`; overlay.style.top = `${top}px`; overlay.style.width = `${imageRect.width}px`; overlay.style.height = `${imageRect.height}px`; } /** * Draws a 7×7 grid (i.e., 8 vertical + 8 horizontal lines) inside #gridOverlay. */ function drawGridOverlay() { const overlay = document.getElementById('gridOverlay'); const img = document.getElementById('uploadedImage'); if (!overlay || !img || img.classList.contains('d-none') || !img.src) return; positionGridOverlayToImage(); // Clear previous lines overlay.innerHTML = ''; const cols = GRID_COLS; // 7 const rows = GRID_ROWS; // 7 // Helper to create line const makeLine = (styleObj) => { const line = document.createElement('div'); line.style.position = 'absolute'; line.style.background = 'rgba(255,255,255,0.6)'; // hairline-ish width line.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.1) inset'; Object.assign(line.style, styleObj); overlay.appendChild(line); }; // Vertical lines (9) for (let i = 0; i <= cols; i++) { const xPct = (i / cols) * 100; makeLine({ top: '0', bottom: '0', width: '1px', left: `calc(${xPct}% - 0.5px)`, }); } // Horizontal lines (9) for (let j = 0; j <= rows; j++) { const yPct = (j / rows) * 100; makeLine({ left: '0', right: '0', height: '1px', top: `calc(${yPct}% - 0.5px)`, }); } } /** * Shows/hides and (re)draws the grid depending on toggle state. */ function updateGridVisibility() { const overlay = document.getElementById('gridOverlay'); if (!overlay) return; if (viewGridEnabled) { overlay.style.display = 'block'; drawGridOverlay(); } else { overlay.style.display = 'none'; overlay.innerHTML = ''; } } // Ensure the toggle reflects current state when modal opens $('#settingsModal').on('shown.bs.modal', function () { $('#toggleViewGrid').prop('checked', viewGridEnabled); }); // Toggle handler $(document).on('change', '#toggleViewGrid', function () { viewGridEnabled = $(this).is(':checked'); updateGridVisibility(); }); $(window).on('resize', function () { if (viewGridEnabled) { drawGridOverlay(); } }); // Redraw the grid whenever the image finishes loading / changes $('#uploadedImage').on('load', function () { if (viewGridEnabled) drawGridOverlay(); updateGridVisibility(); // positions + draws const hi = document.getElementById('gridHighlightOverlay'); if (hi) { hi.style.display = 'none'; } }); function getGridCellFromClick(event) { const img = document.getElementById('uploadedImage'); if (!img || img.classList.contains('d-none') || !img.src) return null; const rect = img.getBoundingClientRect(); const x = event.clientX; const y = event.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { return null; // clicked outside visible image bounds } const dx = (x - rect.left) / rect.width; // 0..1 const dy = (y - rect.top) / rect.height; // 0..1 let col = Math.floor(dx * GRID_COLS); let row = Math.floor(dy * GRID_ROWS); // Clamp just in case of boundary rounding col = Math.max(0, Math.min(GRID_COLS - 1, col)); row = Math.max(0, Math.min(GRID_ROWS - 1, row)); return { row, col }; } $('#uploadedImageContainer').on('click', function (e) { // Ignore if cropping in progress if (typeof isCropping !== 'undefined' && isCropping) return; if (!runId) { logWorkingMessage('No run active yet. Upload/select an image first.', 'text-danger'); return; } const cell = getGridCellFromClick(e); if (!cell) return; const { row, col } = cell; // NEW: spatial feedback showCellHighlight(row, col); logWorkingMessage(`Cell click → row ${row}, col ${col}. Requesting /cell-sim...`, 'text-white'); const params = new URLSearchParams({ runId: runId, row: String(row), col: String(col), K: String(CELL_SIM_K) }); fetch(`${API_BASE_URL}/cell-sim?${params.toString()}`) .then(res => res.json()) .then(data => { logWorkingMessage('Cell similarities received.', 'text-white'); display_sentences(data); }) .catch(err => { console.error('cell-sim error:', err); logWorkingMessage('Error fetching cell similarities.', 'text-danger'); }); }); /** * Briefly highlight a specific grid cell on the visible image. * @param {number} row - 0..GRID_ROWS-1 * @param {number} col - 0..GRID_COLS-1 */ function showCellHighlight(row, col) { const container = document.getElementById('uploadedImageContainer'); const img = document.getElementById('uploadedImage'); const hi = document.getElementById('gridHighlightOverlay'); if (!container || !img || !hi) return; if (img.classList.contains('d-none') || !img.src) return; // Position relative to container, aligned to visible image rect. const containerRect = container.getBoundingClientRect(); const imageRect = img.getBoundingClientRect(); const cellW = imageRect.width / GRID_COLS; const cellH = imageRect.height / GRID_ROWS; const left = (imageRect.left - containerRect.left) + col * cellW; const top = (imageRect.top - containerRect.top) + row * cellH; // Style as an outline box with subtle fill, and fade-out transition. hi.style.left = `${left}px`; hi.style.top = `${top}px`; hi.style.width = `${cellW}px`; hi.style.height = `${cellH}px`; hi.style.border = '2px solid rgba(255, 255, 0, 0.9)'; hi.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.25) inset'; hi.style.background = 'rgba(255, 255, 0, 0.10)'; hi.style.opacity = '1'; hi.style.transition = 'opacity 200ms ease'; hi.style.display = 'block'; // Clear any previous timer, then fade out and hide. if (cellHighlightTimeout) clearTimeout(cellHighlightTimeout); cellHighlightTimeout = setTimeout(() => { hi.style.opacity = '0'; setTimeout(() => { hi.style.display = 'none'; }, 210); }, 600); } // ────────────────────────────────────────────────────────────────────────────── // NEW helper : use a gallery image as the next run // ────────────────────────────────────────────────────────────────────────────── function loadImageAndRun(imgSrc) { // close the modal/backdrop if still open $('#workOverlayBackdrop, #workDetailsModal').remove(); // REMOVED: Save current image to history before loading new one // REMOVED: saveCurrentImageToHistory(); // show the chosen artwork in the main image slot const $img = $('#uploadedImage') .attr('src', imgSrc) .attr('crossorigin', 'anonymous') // allow canvas use .removeClass('d-none'); // hide the upload card / example images just like other entry paths $('#uploadTrigger').addClass('d-none'); $('.card:has(#uploadTrigger)').addClass('d-none'); $('#exampleContainer').addClass('d-none'); // UI bits the normal flow expects $('#workingOverlay').removeClass('d-none'); $('#imageTools').removeClass('d-none'); // make sure we fetch a presign only after the image data is ready $img.one('load', () => fetchPresign()); } // ============================================================================ // Heatmap functionality // ============================================================================ /** * Request heatmap generation for a sentence and display overlay * @param {string} sentence - The sentence text to visualize */ function requestHeatmap(sentence) { if (!runId) { console.error('No active run for heatmap generation'); return; } // Warn if sentence is very long (might be truncated) if (sentence.length > 300) { console.warn('Long sentence will be truncated for heatmap generation'); } // Show loading indicator showHeatmapLoading(); fetch(`${API_BASE_URL}/heatmap`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId: runId, sentence: sentence, layerIdx: -1 // Use last layer by default }) }) .then(res => res.json()) .then(data => { if (data.dataUrl) { displayHeatmapOverlay(data.dataUrl); } else { console.error('No heatmap data received'); hideHeatmapOverlay(); } }) .catch(err => { console.error('Heatmap generation error:', err); hideHeatmapOverlay(); }); } /** * Display heatmap overlay on top of current image * @param {string} dataUrl - Base64 encoded image data URL */ function displayHeatmapOverlay(dataUrl) { // Remove any existing heatmap overlay $('#heatmapOverlay').remove(); const container = $('#uploadedImageContainer'); const img = $('#uploadedImage'); // Create heatmap overlay matching image position const heatmapOverlay = $(`
    `); // Position overlay to match visible image const containerRect = container[0].getBoundingClientRect(); const imageRect = img[0].getBoundingClientRect(); heatmapOverlay.css({ left: imageRect.left - containerRect.left, top: imageRect.top - containerRect.top, width: imageRect.width, height: imageRect.height }); container.append(heatmapOverlay); // Close handlers $('#closeHeatmapBtn, #heatmapOverlay').on('click', function(e) { if (e.target === this || $(e.target).closest('#closeHeatmapBtn').length) { hideHeatmapOverlay(); } }); } /** * Show loading indicator while heatmap is being generated */ function showHeatmapLoading() { $('#heatmapOverlay').remove(); const container = $('#uploadedImageContainer'); const img = $('#uploadedImage'); const loadingOverlay = $(`

    Generating heatmap...

    `); // Position to match image const containerRect = container[0].getBoundingClientRect(); const imageRect = img[0].getBoundingClientRect(); loadingOverlay.css({ left: imageRect.left - containerRect.left, top: imageRect.top - containerRect.top, width: imageRect.width, height: imageRect.height }); container.append(loadingOverlay); } /** * Remove heatmap overlay */ function hideHeatmapOverlay() { $('#heatmapOverlay').fadeOut(200, function() { $(this).remove(); }); } // Update heatmap position when window resizes $(window).on('resize', function() { const heatmapOverlay = $('#heatmapOverlay'); if (heatmapOverlay.length && !heatmapOverlay.find('.spin').length) { // Reposition existing heatmap const container = $('#uploadedImageContainer'); const img = $('#uploadedImage'); const containerRect = container[0].getBoundingClientRect(); const imageRect = img[0].getBoundingClientRect(); heatmapOverlay.css({ left: imageRect.left - containerRect.left, top: imageRect.top - containerRect.top, width: imageRect.width, height: imageRect.height }); } }); // --- Begin Landing Content Functionality --- // Hide landing content (upload card + example images) function hideLandingContent() { $('#landingContent').addClass('d-none'); } // Show landing content (upload card + example images) function showLandingContent() { $('#landingContent').removeClass('d-none'); $('#exampleContainer').removeClass('d-none'); $('.card:has(#uploadTrigger)').removeClass('d-none'); } // --- End Landing Content Functionality ---