| |
| |
| |
| const API_BASE_URL = ""; |
|
|
| |
| let runId; |
| let imageKey; |
| let upload; |
|
|
| |
| let viewGridEnabled = false; |
|
|
| const GRID_ROWS = 7; |
| const GRID_COLS = 7; |
| const CELL_SIM_K = 25; |
|
|
| |
| let availableModels = []; |
| let selectedModel = ''; |
| let creatorsMap = {}; |
|
|
| let selectedCreators = []; |
|
|
|
|
| |
| let cellHighlightTimeout = null; |
|
|
| function updateCreatorTags() { |
| const tagContainer = $('#creatorTags'); |
| tagContainer.empty(); |
| selectedCreators.forEach(name => { |
| const tag = $('<span>') |
| .addClass('badge bg-primary px-3 py-2 d-flex align-items-center') |
| .html(`${name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} <i class="bi bi-x ms-2" style="cursor:pointer;"></i>`); |
| tag.find('i').on('click', function () { |
| selectedCreators = selectedCreators.filter(c => c !== name); |
| updateCreatorTags(); |
| }); |
| tagContainer.append(tag); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| function logWorkingMessage(message, type = 'text-white') { |
| const logContainer = $('#workingLog'); |
| if (!logContainer.length) { |
| console.log('[WORKING]', message); |
| return; |
| } |
| logContainer.append(`<div class="${type}">${message}</div>`); |
| logContainer.scrollTop(logContainer[0].scrollHeight); |
| } |
|
|
| |
| |
| |
| let selectedTopics = []; |
| let topicMap = {}; |
|
|
| |
| |
| |
| |
| 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 = $('<button>') |
| .addClass('btn btn-sm px-3 py-1 rounded-pill') |
| .addClass(isSelected ? 'btn-primary' : 'btn-outline-secondary') |
| .text(label) |
| .data('code', code) |
| .on('click', function () { |
| const idx = selectedTopics.indexOf(code); |
| if (idx === -1) { |
| selectedTopics.push(code); |
| } else { |
| selectedTopics.splice(idx, 1); |
| } |
| updateSelectedTopicsDisplay(); |
| $(`#topicTags button[data-code="${code}"]`) |
| .toggleClass('active') |
| .toggleClass('btn-primary') |
| .toggleClass('btn-outline-primary'); |
| }); |
| selectedTagContainer.append(tag); |
| } |
| } |
|
|
| |
| function updateSelectedCreatorsDisplay() { |
| $('#selectedCreatorsWrapper').removeClass('d-none'); |
| const tagContainer = $('#selectedCreatorTags'); |
| tagContainer.empty(); |
| selectedCreators.forEach(name => { |
| const tag = $('<span>') |
| .addClass('badge bg-primary px-3 py-2 d-flex align-items-center') |
| .html(`${name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} <i class="bi bi-x ms-2" style="cursor:pointer;"></i>`); |
| tag.find('i').on('click', function () { |
| selectedCreators = selectedCreators.filter(c => c !== name); |
| updateSelectedCreatorsDisplay(); |
| }); |
| tagContainer.append(tag); |
| }); |
| } |
|
|
| |
| $(document).ready(function () { |
| |
| $('.navbar-brand').on('click', function (e) { |
| e.preventDefault(); |
|
|
| |
| runId = null; |
| imageKey = null; |
| upload = null; |
| viewGridEnabled = false; |
| selectedTopics = []; |
| selectedCreators = []; |
| selectedModel = ''; |
|
|
| |
| $('#uploadedImage').addClass('d-none').attr('src', ''); |
| $('#uploadTrigger').removeClass('d-none'); |
| $('#imageTools').addClass('d-none'); |
| $('#workingOverlay').addClass('d-none'); |
| $('#workDetailsBanner').remove(); |
| $('#gridOverlay').hide().html(''); |
| $('#gridHighlightOverlay').hide(); |
| $('#heatmapOverlay').remove(); |
|
|
| |
| $('.col-md-3').addClass('d-none'); |
| $('#sentenceList').empty(); |
| $('#imageHistoryWrapper').addClass('d-none'); |
| |
| $('#selectedTopicsWrapper').addClass('d-none'); |
| $('#selectedCreatorsWrapper').addClass('d-none'); |
|
|
| |
| $('#topicTags button').removeClass('active btn-primary').addClass('btn-outline-primary'); |
| $('#selectedTopicTags').empty(); |
|
|
| |
| $('#creatorTags').empty(); |
| $('#selectedCreatorTags').empty(); |
| $('#creatorSearch').val(''); |
| $('#creatorSearchResults').empty(); |
| $('#creatorPanelSearch').val(''); |
| $('#creatorPanelResults').empty(); |
|
|
| |
| if (availableModels.length > 0) { |
| selectedModel = availableModels[0]; |
| $('#modelDropdown').text('AI Model: ' + selectedModel); |
| $('#modelDropdownMenu a').removeClass('active'); |
| $('#modelDropdownMenu a').first().addClass('active'); |
| } |
|
|
| |
| $('#debugStatus').text('Idle'); |
| $('#debugSessionId').text('N/A'); |
| $('#workingLog').empty(); |
|
|
| |
| if ($('.card:has(#uploadTrigger)').length === 0 && $('#exampleContainer').length === 0) { |
| const uploadCard = $(` |
| <div class="card h-100 text-center d-flex align-items-center justify-content-center" style="cursor: pointer; background-color: rgba(255,255,255,0.1);"> |
| <div class="card-body"> |
| <p class="mb-2">Drop an image here or click to upload</p> |
| <button class="btn btn-primary" id="uploadTrigger"> |
| <i class="bi bi-upload"></i> Upload Image |
| </button> |
| </div> |
| </div> |
| `); |
| $('#uploadedImageContainer').prepend(uploadCard); |
| } |
|
|
| |
| showLandingContent(); |
| adjustMainWidth(); |
| }); |
|
|
| |
| 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); |
| }); |
|
|
| |
| fetch(`${API_BASE_URL}/models`) |
| .then(response => response.json()) |
| .then(data => { |
| availableModels = data; |
| console.log("Available models:", availableModels); |
| |
| const dropdownMenu = $('#modelDropdownMenu'); |
| if (availableModels.length > 0) { |
| dropdownMenu.empty(); |
| availableModels.forEach((model, index) => { |
| const item = $('<li><a class="dropdown-item" href="#">' + model + '</a></li>'); |
| 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); |
| }); |
|
|
| |
| 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); |
| }); |
|
|
| |
| $('#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 = $('<button>') |
| .addClass('list-group-item list-group-item-action') |
| .text(name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())) |
| .on('click', function () { |
| if (!selectedCreators.includes(name)) { |
| selectedCreators.push(name); |
| updateCreatorTags(); |
| } |
| $('#creatorSearch').val(''); |
| resultsContainer.empty(); |
| }); |
| resultsContainer.append(item); |
| }); |
| } |
| }); |
|
|
| |
| $('#creatorSearch').on('keydown', function (e) { |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| const firstItem = $('#creatorSearchResults button').first(); |
| if (firstItem.length) { |
| firstItem.click(); |
| } |
| } |
| }); |
|
|
| |
| $('#creatorPanelSearch').on('input', function () { |
| const query = $(this).val().toLowerCase(); |
| const resultsContainer = $('#creatorPanelResults'); |
| resultsContainer.empty(); |
| if (query.length > 0) { |
| const matches = Object.keys(creatorsMap).filter(name => |
| name.toLowerCase().includes(query) |
| ); |
| matches.forEach((name, index) => { |
| const item = $('<button>') |
| .addClass('list-group-item list-group-item-action') |
| .text(name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())) |
| .on('click', function () { |
| if (!selectedCreators.includes(name)) { |
| selectedCreators.push(name); |
| updateSelectedCreatorsDisplay(); |
| } |
| $('#creatorPanelSearch').val(''); |
| resultsContainer.empty(); |
| }); |
| if (index === 0) { |
| item.addClass('active'); |
| } |
| resultsContainer.append(item); |
| }); |
| } |
| }); |
|
|
| $('#creatorPanelSearch').on('keydown', function (e) { |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| const firstItem = $('#creatorPanelResults button').first(); |
| if (firstItem.length) { |
| firstItem.click(); |
| } |
| } |
| }); |
| |
| $('#uploadTrigger').on('click', function () { |
| $('#imageUpload').click(); |
| }); |
|
|
| |
| $('#imageUpload').on('change', function (event) { |
| const file = event.target.files[0]; |
| if (file) { |
| const reader = new FileReader(); |
| reader.onload = function (e) { |
| $('#uploadedImage').attr('src', e.target.result).removeClass('d-none'); |
| $('#uploadTrigger').addClass('d-none'); |
| $('.card:has(#uploadTrigger)').addClass('d-none'); |
| $('#exampleContainer').addClass('d-none'); |
| $('#workingOverlay').removeClass('d-none'); |
| $('#imageTools').removeClass('d-none'); |
| |
| |
| $('#uploadedImage').on('load', function() { |
| fetchPresign(); |
| }); |
| |
| |
| setTimeout(() => { |
| if ($('#uploadedImage').attr('src')) { |
| fetchPresign(); |
| } |
| }, 1000); |
| }; |
| reader.readAsDataURL(file); |
| } |
| }); |
|
|
| |
| $('#uploadedImageContainer').on('dragover', function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| $(this).addClass('border border-light'); |
| }); |
|
|
| $('#uploadedImageContainer').on('dragleave', function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| $(this).removeClass('border border-light'); |
| }); |
|
|
| $('#uploadedImageContainer').on('drop', function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| $(this).removeClass('border border-light'); |
| const file = e.originalEvent.dataTransfer.files[0]; |
| if (file && file.type.startsWith('image/')) { |
| const reader = new FileReader(); |
| reader.onload = function (e) { |
| $('#uploadedImage').attr('src', e.target.result).removeClass('d-none'); |
| $('#uploadTrigger').addClass('d-none'); |
| $('#workingOverlay').removeClass('d-none'); |
| $('#imageTools').removeClass('d-none'); |
| fetchPresign(); |
| }; |
| reader.readAsDataURL(file); |
| } |
| }); |
|
|
| |
| let selectedSrc = null; |
| $('.example-img').on('click', function () { |
| $('.example-img').removeClass('selected-img'); |
| $(this).addClass('selected-img'); |
| selectedSrc = $(this).attr('src'); |
| $('#selectImageBtn').removeClass('d-none'); |
| }); |
|
|
| |
| $('#selectImageBtn').on('click', function () { |
| if (selectedSrc) { |
| $('#uploadedImage').attr('src', selectedSrc).removeClass('d-none'); |
| $('#uploadTrigger').addClass('d-none'); |
| $('.card:has(#uploadTrigger)').addClass('d-none'); |
| $('#exampleContainer').addClass('d-none'); |
| $('#workingOverlay').removeClass('d-none'); |
| $('#imageTools').removeClass('d-none'); |
| fetchPresign(); |
| } |
| }); |
|
|
| |
| adjustMainWidth(); |
|
|
| |
| $('.nav-link[href="#"]:contains("About")').on('click', function(e) { |
| e.preventDefault(); |
| $('#aboutModal').modal('show'); |
| }); |
| }); |
|
|
| |
| |
| |
| |
| function fetchPresign() { |
| try { |
| $('#debugStatus').text('Requesting ID...'); |
| logWorkingMessage('Requesting Session ID...', 'text-white'); |
|
|
| |
| saveCurrentImageToHistory(); |
|
|
| fetch(`${API_BASE_URL}/presign`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ fileName: 'selected.jpg' }) |
| }) |
| .then(res => { |
| if (!res.ok) { |
| throw new Error(`Presign failed: ${res.status}`); |
| } |
| return res.json(); |
| }) |
| .then(data => { |
| runId = data.runId; |
| imageKey = data.imageKey; |
| upload = data.upload; |
|
|
| |
| const imgElement = document.getElementById('uploadedImage'); |
| const canvas = document.createElement('canvas'); |
| canvas.width = imgElement.naturalWidth; |
| canvas.height = imgElement.naturalHeight; |
| const ctx = canvas.getContext('2d'); |
| ctx.drawImage(imgElement, 0, 0); |
|
|
| canvas.toBlob(function (blob) { |
| const file = new File([blob], 'uploaded.jpg', { type: blob.type }); |
| const formData = new FormData(); |
| formData.append('file', file); |
|
|
| |
| hideLandingContent(); |
|
|
| fetch(`${API_BASE_URL}/upload/${runId}`, { |
| method: 'POST', |
| body: formData |
| }) |
| .then(res => { |
| if (res.status === 204) { |
| logWorkingMessage('Image uploaded successfully (204 No Content)', 'text-white'); |
| } else if (res.ok) { |
| |
| return res.json().then(() => { |
| logWorkingMessage('Image uploaded successfully', 'text-white'); |
| }); |
| } else { |
| |
| logWorkingMessage(`Upload failed with status: ${res.status}`, 'text-danger'); |
| throw new Error(`Upload failed: ${res.status}`); |
| } |
| }) |
| .then(() => { |
| $('#debugStatus').text('Got ID'); |
| logWorkingMessage('Session ID: ' + runId, 'text-white'); |
| logWorkingMessage('Sending /runs request...', 'text-white'); |
| $('#debugStatus').text('Posting run info'); |
|
|
| |
| updateSelectedTopicsDisplay(); |
| updateSelectedCreatorsDisplay(); |
| |
| |
| showBottomCards(); |
| adjustMainWidth(); |
|
|
| return fetch(`${API_BASE_URL}/runs`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| runId: runId, |
| imageKey: imageKey, |
| topics: selectedTopics, |
| creators: selectedCreators, |
| model: selectedModel |
| }) |
| }); |
| }) |
| .then(res => { |
| |
| if (res.status === 202) { |
| |
| return { status: 'accepted' }; |
| } |
| return res.json(); |
| }) |
| .then(response => { |
| logWorkingMessage('Run registered successfully', 'text-white'); |
| $('#debugStatus').text('Run submitted'); |
| |
| |
| if (response.sentences && response.sentences.length > 0) { |
| logWorkingMessage('Stub mode: displaying sentences directly', 'text-white'); |
| display_sentences(response); |
| $('#workingOverlay').addClass('d-none'); |
| } else if (response.status === 'accepted') { |
| |
| logWorkingMessage('Run accepted, starting to poll for results...', 'text-white'); |
| pollRunStatus(runId); |
| } else { |
| |
| pollRunStatus(runId); |
| } |
| }) |
| .catch(err => { |
| console.error('Upload or /runs error:', err); |
| logWorkingMessage('Error uploading image or submitting run', 'text-danger'); |
| $('#debugStatus').text('Run submission failed'); |
| }); |
| }, 'image/jpeg'); |
| |
| }) |
| .catch(err => { |
| console.error('Presign error:', err); |
| $('#debugStatus').text('Error fetching ID'); |
| logWorkingMessage('Error fetching ID', 'text-danger'); |
| |
| $('#workingOverlay').addClass('d-none'); |
| $('#uploadTrigger').removeClass('d-none'); |
| }); |
| } catch (err) { |
| console.error('Error in fetchPresign:', err); |
| logWorkingMessage('Error in fetchPresign: ' + err.message, 'text-danger'); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function pollRunStatus(runId) { |
| if (!runId) { |
| console.error('pollRunStatus called with invalid runId:', runId); |
| return; |
| } |
| |
| logWorkingMessage('Polling run status...', 'text-white'); |
|
|
| const intervalId = setInterval(() => { |
| fetch(`${API_BASE_URL}/runs/${runId}`) |
| .then(res => { |
| if (!res.ok) { |
| throw new Error(`Status check failed: ${res.status}`); |
| } |
| return res.json(); |
| }) |
| .then(data => { |
| $('#debugStatus').text(`Status: ${data.status}`); |
| logWorkingMessage(`Status: ${data.status}`, 'text-white'); |
|
|
| if (data.status !== 'processing') { |
| clearInterval(intervalId); |
| logWorkingMessage('Processing complete', 'text-white'); |
|
|
| if (data.status === 'done') { |
| |
| const filePath = data.outputKey ? data.outputKey.split('/').pop() : `${runId}.json`; |
| |
| logWorkingMessage('Fetching outputs from: ' + filePath, 'text-white'); |
|
|
| |
| fetch(`${API_BASE_URL}/outputs/${filePath}`) |
| .then(res => res.json()) |
| .then(output => { |
| logWorkingMessage('Outputs received', 'text-white'); |
| console.log('Raw output data:', output); |
| display_sentences(output); |
| $('#workingOverlay').addClass('d-none'); |
| }) |
| .catch(err => { |
| console.error('Error fetching outputs:', err); |
| logWorkingMessage('Error fetching outputs', 'text-danger'); |
| }); |
| } |
| else if (data.status === 'error') { |
| logWorkingMessage('An error occurred during processing.', 'text-danger'); |
| setTimeout(() => { |
| $('#workingOverlay').addClass('d-none'); |
| |
| |
| |
| |
| |
| |
| $('#debugStatus').text('Idle'); |
| $('#debugSessionId').text('N/A'); |
| selectedTopics = []; |
| |
| |
| }, 5000); |
| } |
| } |
| }) |
| .catch(err => { |
| console.error('Polling error:', err); |
| logWorkingMessage('Error polling status: ' + err.message, 'text-danger'); |
| clearInterval(intervalId); |
| |
| $('#workingOverlay').addClass('d-none'); |
| }); |
| }, 1000); |
| } |
|
|
| |
| |
| |
| |
| |
| function escapeHTML(str) { |
| |
| if (typeof str !== 'string') { |
| console.warn('escapeHTML called with non-string:', str); |
| return String(str || ''); |
| } |
| return str.replace(/[&<>'"]/g, tag => ( |
| {'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'}[tag] |
| )); |
| } |
|
|
| |
| |
| |
| |
| function display_sentences(data) { |
| try { |
| console.log('display_sentences called with:', data); |
| |
| |
| if (!data) { |
| console.warn('display_sentences called with null/undefined data'); |
| $('#workingOverlay').addClass('d-none'); |
| return; |
| } |
| |
| |
| if (!Array.isArray(data)) { |
| data = (data && Array.isArray(data.sentences)) ? data.sentences : []; |
| } |
| console.log('Normalized data:', data); |
| |
| if (!data.length) { |
| $('#workingOverlay').addClass('d-none'); |
| return; |
| } |
|
|
| |
| $('.col-md-3').removeClass('d-none'); |
| $('#sentenceList').empty(); |
|
|
| |
| data.forEach((item, index) => { |
| console.log(`Processing item ${index}:`, item); |
| |
| |
| if (!item.english_original || typeof item.english_original !== 'string') { |
| console.warn(`Item ${index} has invalid english_original:`, item.english_original); |
| return; |
| } |
| |
| const li = $(` |
| <li class="list-group-item sentence-item mb-1" |
| data-work="${item.work || 'unknown'}" |
| data-sentence="${escapeHTML(item.english_original)}"> |
| <div class="d-flex align-items-center"> |
| <span class="flex-grow-1">${escapeHTML(item.english_original)}</span> |
| <button class="btn btn-sm btn-outline-dark ms-2 heatmap-btn" |
| title="View heatmap" |
| data-sentence="${escapeHTML(item.english_original)}"> |
| <i class="bi bi-thermometer-half"></i> |
| </button> |
| </div> |
| </li> |
| `); |
| 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'); |
| } |
| } |
|
|
| |
| function adjustMainWidth(){ |
| const $main = $('#uploadedImageContainer').closest('.col-md-9'); |
| if ($('.col-md-3').hasClass('d-none')){ |
| $main.addClass('fill'); |
| } else{ |
| $main.removeClass('fill'); |
| } |
| } |
|
|
| |
| |
| |
| function showBottomCards() { |
| $('#imageHistoryWrapper').removeClass('d-none'); |
| $('#selectedTopicsWrapper').removeClass('d-none'); |
| $('#selectedCreatorsWrapper').removeClass('d-none'); |
| } |
|
|
| |
| |
| |
| function saveCurrentImageToHistory() { |
| const currentImg = $('#uploadedImage'); |
| if (!currentImg.attr('src') || currentImg.hasClass('d-none')) { |
| return; |
| } |
| |
| |
| const firstHistoryImg = $('#imageHistory img').first(); |
| if (firstHistoryImg.length && firstHistoryImg.attr('src') === currentImg.attr('src')) { |
| return; |
| } |
| |
| 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); |
| } |
|
|
| |
| |
| let isCropping = false; |
| let cropStartX = 0; |
| let cropStartY = 0; |
| let cropRect = null; |
|
|
| |
| $('#cropToolBtn').on('click', function () { |
| isCropping = true; |
| $('#uploadedImageContainer').css('cursor', 'crosshair'); |
| }); |
|
|
| |
| $('#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 = $('<div>') |
| .addClass('position-absolute border border-warning') |
| .css({ |
| left: cropStartX, |
| top: cropStartY, |
| width: 0, |
| height: 0, |
| zIndex: 10, |
| pointerEvents: 'none' |
| }) |
| .appendTo('#uploadedImageContainer'); |
| }); |
|
|
| |
| $('#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 }); |
| }); |
|
|
| |
| $('#uploadedImageContainer').on('mouseup', function (e) { |
| if (!isCropping || !cropRect) return; |
| isCropping = false; |
| $('#uploadedImageContainer').css('cursor', 'default'); |
|
|
| const img = document.getElementById('uploadedImage'); |
| |
| const imageRect = img.getBoundingClientRect(); |
| const cropOffset = cropRect.offset(); |
|
|
| |
| 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; |
|
|
| |
| if (sw <= 0 || sh <= 0) { |
| cropRect.remove(); |
| cropRect = null; |
| return; |
| } |
|
|
| |
| |
|
|
| |
| 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(); |
| }); |
| |
|
|
| |
| |
| $('#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(); |
| } |
| }); |
|
|
| |
|
|
| |
| |
| $('#imageHistory').on('click', 'img', function () { |
| const currentImg = $('#uploadedImage')[0]; |
| const newSrc = $(this).attr('src'); |
| |
| |
| |
| |
| |
|
|
| |
| $('#uploadedImage').attr('src', newSrc).removeClass('d-none'); |
|
|
| |
| $('#workingOverlay').removeClass('d-none'); |
| logWorkingMessage('Rerunning with selected image from history...', 'text-white'); |
| fetchPresign(); |
| }); |
| |
|
|
| |
| |
| $('#rerunToolBtn').on('click', function () { |
| $('#workingOverlay').removeClass('d-none'); |
| logWorkingMessage('Rerunning with current image...', 'text-white'); |
| fetchPresign(); |
| }); |
| |
|
|
| |
| $('#gridToolBtn').on('click', function () { |
| viewGridEnabled = !viewGridEnabled; |
| updateGridVisibility(); |
| $(this).toggleClass('active'); |
| }); |
|
|
| |
| |
| |
| |
| 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); |
| }) |
| .catch(console.error); |
| } |
|
|
| |
| |
| |
| |
| function showWorkDetails(workData) { |
| |
| $('#workOverlayBackdrop, #workDetailsModal').remove(); |
|
|
| const d = workData; |
| const ctxHtml = d.context |
| ? `<section class="mb-3"> |
| <p class="fw-bold mb-1">In Context</p> |
| <blockquote class="context-paragraph" |
| style="white-space:pre-wrap;"> |
| ${escapeHTML(d.context)} |
| </blockquote> |
| </section>` |
| : ''; |
|
|
| |
| const backdrop = $( |
| `<div id="workOverlayBackdrop" |
| class="position-fixed top-0 start-0 w-100 h-100 |
| bg-dark bg-opacity-50" |
| style="z-index:2000;"></div>` |
| ); |
|
|
| const modal = $(` |
| <div id="workDetailsModal" |
| class="position-fixed bg-white border border-primary rounded shadow p-4" |
| style="top:50%; left:50%; transform:translate(-50%,-50%); |
| max-width:90vw; max-height:80vh; overflow:auto; z-index:2001;"> |
| |
| <!-- close button --> |
| <button type="button" |
| class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 m-2" |
| id="workDetailsClose"> |
| <i class="bi bi-x-lg"></i> |
| </button> |
| |
| <h5 class="mb-2">${d.Work_Title || 'Unknown Title'}</h5> |
| <p class="mb-1"><strong>Author(s):</strong> ${d.Author_Name || 'Unknown Author'}</p> |
| <p class="mb-1"><strong>Year:</strong> ${d.Year || 'Unknown'}</p> |
| |
| ${ctxHtml} <!-- ← NEW paragraph section --> |
| |
| <!-- Image gallery (unchanged) --> |
| <div id="galleryWrapper" class="mb-2"> |
| <div class="fw-bold">Images in this work</div> |
| <div id="galleryScroller" class="mt-1"></div> |
| </div> |
| |
| <p class="mb-1"><strong>DOI:</strong> |
| <a href="${d.DOI}" target="_blank" class="text-primary text-decoration-underline">${d.DOI}</a> |
| </p> |
| <p class="mb-1"><strong>Download Link:</strong> |
| <a href="${d.Link}" target="_blank" class="text-primary text-decoration-underline">${d.Link}</a> |
| </p> |
| |
| <!-- BibTeX block (unchanged) --> |
| <div class="position-relative mt-3"> |
| <span class="fw-bold">BibTeX Citation</span> |
| <button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0" |
| onclick="copyBibTeX()" title="Copy to clipboard"> |
| <i class="bi bi-clipboard"></i> |
| </button> |
| <pre id="bibtexContent" |
| class="p-2 mt-1 rounded" |
| style="white-space:pre-wrap; word-break:break-word; |
| font-size:.875rem; background:#fdfde7; color:#000; padding-right:3rem;"> |
| ${d.BibTeX || 'Citation not available'} |
| </pre> |
| </div> |
| |
| <iframe src="${d.DOI}" |
| style="width:100%; height:50vh; border:none;" |
| class="mt-3"></iframe> |
| </div> |
| `); |
|
|
| |
| $('body').append(backdrop, modal); |
|
|
| |
| 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 => $('<img>') |
| .attr('src', u) |
| .attr('crossorigin', 'anonymous') |
| .addClass('img-thumbnail') |
| .css({ height: '120px', cursor: 'pointer' }) |
| .on('click', () => loadImageAndRun(u)) |
| .appendTo(scroller)); |
| }) |
| .catch(console.error); |
| } |
|
|
| |
| backdrop.on('click', () => { backdrop.remove(); modal.remove(); }); |
| modal.on('click', '#workDetailsClose', () => { backdrop.remove(); modal.remove(); }); |
| } |
|
|
| |
| function copyBibTeX() { |
| const bibtexText = document.getElementById('bibtexContent').textContent.trim(); |
| |
| |
| const tempTextarea = document.createElement('textarea'); |
| tempTextarea.value = bibtexText; |
| tempTextarea.style.position = 'fixed'; |
| tempTextarea.style.opacity = '0'; |
| document.body.appendChild(tempTextarea); |
| |
| |
| tempTextarea.select(); |
| document.execCommand('copy'); |
| document.body.removeChild(tempTextarea); |
| |
| |
| const copyBtn = event.target.closest('button'); |
| const icon = copyBtn.querySelector('i'); |
| icon.classList.remove('bi-clipboard'); |
| icon.classList.add('bi-clipboard-check'); |
| |
| |
| copyBtn.setAttribute('title', 'Copied!'); |
| |
| |
| setTimeout(() => { |
| icon.classList.remove('bi-clipboard-check'); |
| icon.classList.add('bi-clipboard'); |
| copyBtn.setAttribute('title', 'Copy to clipboard'); |
| }, 2000); |
| } |
|
|
| |
| |
| |
| 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(); |
| |
| 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`; |
| } |
|
|
| |
| |
| |
| function drawGridOverlay() { |
| const overlay = document.getElementById('gridOverlay'); |
| const img = document.getElementById('uploadedImage'); |
| if (!overlay || !img || img.classList.contains('d-none') || !img.src) return; |
|
|
| positionGridOverlayToImage(); |
|
|
| |
| overlay.innerHTML = ''; |
|
|
| const cols = GRID_COLS; |
| const rows = GRID_ROWS; |
|
|
| |
| const makeLine = (styleObj) => { |
| const line = document.createElement('div'); |
| line.style.position = 'absolute'; |
| line.style.background = 'rgba(255,255,255,0.6)'; |
| |
| line.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.1) inset'; |
| Object.assign(line.style, styleObj); |
| overlay.appendChild(line); |
| }; |
|
|
| |
| for (let i = 0; i <= cols; i++) { |
| const xPct = (i / cols) * 100; |
| makeLine({ |
| top: '0', |
| bottom: '0', |
| width: '1px', |
| left: `calc(${xPct}% - 0.5px)`, |
| }); |
| } |
|
|
| |
| for (let j = 0; j <= rows; j++) { |
| const yPct = (j / rows) * 100; |
| makeLine({ |
| left: '0', |
| right: '0', |
| height: '1px', |
| top: `calc(${yPct}% - 0.5px)`, |
| }); |
| } |
| } |
|
|
| |
| |
| |
| function updateGridVisibility() { |
| const overlay = document.getElementById('gridOverlay'); |
| if (!overlay) return; |
| if (viewGridEnabled) { |
| overlay.style.display = 'block'; |
| drawGridOverlay(); |
| } else { |
| overlay.style.display = 'none'; |
| overlay.innerHTML = ''; |
| } |
| } |
|
|
| |
| $('#settingsModal').on('shown.bs.modal', function () { |
| $('#toggleViewGrid').prop('checked', viewGridEnabled); |
| }); |
|
|
| |
| $(document).on('change', '#toggleViewGrid', function () { |
| viewGridEnabled = $(this).is(':checked'); |
| updateGridVisibility(); |
| }); |
|
|
| $(window).on('resize', function () { |
| if (viewGridEnabled) { |
| drawGridOverlay(); |
| } |
| }); |
| |
| $('#uploadedImage').on('load', function () { |
| if (viewGridEnabled) drawGridOverlay(); |
| updateGridVisibility(); |
| 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; |
| } |
|
|
| const dx = (x - rect.left) / rect.width; |
| const dy = (y - rect.top) / rect.height; |
|
|
| let col = Math.floor(dx * GRID_COLS); |
| let row = Math.floor(dy * GRID_ROWS); |
|
|
| |
| 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) { |
| |
| 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; |
|
|
| |
| 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'); |
| }); |
| }); |
|
|
|
|
| |
| |
| |
| |
| |
| 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; |
|
|
| |
| 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; |
|
|
| |
| 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'; |
|
|
| |
| if (cellHighlightTimeout) clearTimeout(cellHighlightTimeout); |
| cellHighlightTimeout = setTimeout(() => { |
| hi.style.opacity = '0'; |
| setTimeout(() => { |
| hi.style.display = 'none'; |
| }, 210); |
| }, 600); |
| } |
|
|
| |
| |
| |
| function loadImageAndRun(imgSrc) { |
| |
| $('#workOverlayBackdrop, #workDetailsModal').remove(); |
|
|
| |
| |
|
|
| |
| const $img = $('#uploadedImage') |
| .attr('src', imgSrc) |
| .attr('crossorigin', 'anonymous') |
| .removeClass('d-none'); |
|
|
| |
| $('#uploadTrigger').addClass('d-none'); |
| $('.card:has(#uploadTrigger)').addClass('d-none'); |
| $('#exampleContainer').addClass('d-none'); |
|
|
| |
| $('#workingOverlay').removeClass('d-none'); |
| $('#imageTools').removeClass('d-none'); |
|
|
| |
| $img.one('load', () => fetchPresign()); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function requestHeatmap(sentence) { |
| if (!runId) { |
| console.error('No active run for heatmap generation'); |
| return; |
| } |
|
|
| |
| if (sentence.length > 300) { |
| console.warn('Long sentence will be truncated for heatmap generation'); |
| } |
|
|
| |
| showHeatmapLoading(); |
|
|
| fetch(`${API_BASE_URL}/heatmap`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| runId: runId, |
| sentence: sentence, |
| layerIdx: -1 |
| }) |
| }) |
| .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(); |
| }); |
| } |
|
|
| |
| |
| |
| |
| function displayHeatmapOverlay(dataUrl) { |
| |
| $('#heatmapOverlay').remove(); |
| |
| const container = $('#uploadedImageContainer'); |
| const img = $('#uploadedImage'); |
| |
| |
| const heatmapOverlay = $(` |
| <div id="heatmapOverlay" class="position-absolute" style="z-index: 20;"> |
| <img src="${dataUrl}" |
| style="width: 100%; height: 100%; object-fit: contain;" |
| class="heatmap-image" /> |
| <button class="btn btn-sm btn-dark position-absolute top-0 end-0 m-2" |
| id="closeHeatmapBtn" |
| title="Close heatmap"> |
| <i class="bi bi-x-lg"></i> |
| </button> |
| </div> |
| `); |
| |
| |
| 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); |
| |
| |
| $('#closeHeatmapBtn, #heatmapOverlay').on('click', function(e) { |
| if (e.target === this || $(e.target).closest('#closeHeatmapBtn').length) { |
| hideHeatmapOverlay(); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function showHeatmapLoading() { |
| $('#heatmapOverlay').remove(); |
| |
| const container = $('#uploadedImageContainer'); |
| const img = $('#uploadedImage'); |
| |
| const loadingOverlay = $(` |
| <div id="heatmapOverlay" class="position-absolute d-flex align-items-center justify-content-center" |
| style="z-index: 20; background: rgba(0, 0, 0, 0.5);"> |
| <div class="text-white text-center"> |
| <i class="bi bi-arrow-repeat spin" style="font-size: 2rem;"></i> |
| <p class="mt-2">Generating heatmap...</p> |
| </div> |
| </div> |
| `); |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| function hideHeatmapOverlay() { |
| $('#heatmapOverlay').fadeOut(200, function() { |
| $(this).remove(); |
| }); |
| } |
|
|
| |
| $(window).on('resize', function() { |
| const heatmapOverlay = $('#heatmapOverlay'); |
| if (heatmapOverlay.length && !heatmapOverlay.find('.spin').length) { |
| |
| 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 |
| }); |
| } |
| }); |
|
|
| |
| |
| function hideLandingContent() { |
| $('#landingContent').addClass('d-none'); |
| } |
|
|
| |
| function showLandingContent() { |
| $('#landingContent').removeClass('d-none'); |
| $('#exampleContainer').removeClass('d-none'); |
| $('.card:has(#uploadTrigger)').removeClass('d-none'); |
| } |
| |