// Fetch and display training project name in nav bar window.addEventListener('DOMContentLoaded', () => { const urlParams = new URLSearchParams(window.location.search); const trainingProjectId = urlParams.get('id'); if (!trainingProjectId) return; // Fetch training project, details, and all LabelStudioProjects Promise.all([ fetch(`/api/training-projects`).then(res => res.json()), fetch(`/api/training-project-details`).then(res => res.json()), fetch(`/api/label-studio-projects`).then(res => res.json()) ]).then(([projects, detailsList, labelStudioProjects]) => { // Find the selected training project const project = projects.find(p => p.project_id == trainingProjectId || p.id == trainingProjectId); // Find the details entry for this project const details = detailsList.find(d => d.project_id == trainingProjectId); if (!project || !details) return; // Get the stored classes from training project const storedClasses = Array.isArray(project.classes) ? project.classes : []; // Get related LabelStudioProject IDs const relatedIds = Array.isArray(details.annotation_projects) ? details.annotation_projects : []; // Filter LabelStudioProjects to only those related const relatedProjects = labelStudioProjects.filter(lp => relatedIds.includes(lp.project_id)); // Render cards for each related LabelStudioProject const detailsDiv = document.getElementById('details'); detailsDiv.innerHTML = ''; // Find the longest label name for sizing let maxLabelLength = 0; relatedProjects.forEach(lp => { const classNames = Object.keys(lp.annotationCounts || {}); classNames.forEach(className => { if (className && className.trim() !== '' && className.length > maxLabelLength) { maxLabelLength = className.length; } }); }); // Use ch unit for width to fit the longest text const labelWidth = `${maxLabelLength + 2}ch`; // Find the longest project name for sizing let maxProjectNameLength = 0; relatedProjects.forEach(lp => { const nameLength = (lp.title || String(lp.project_id)).length; if (nameLength > maxProjectNameLength) maxProjectNameLength = nameLength; }); const projectNameWidth = `${maxProjectNameLength + 2}ch`; // Find the card with the most classes let maxClassCount = 0; relatedProjects.forEach(lp => { const classNames = Object.keys(lp.annotationCounts || {}); if (classNames.length > maxClassCount) maxClassCount = classNames.length; }); // Set a fixed width for the class rows container const classRowHeight = 38; // px, adjust if needed const classRowsWidth = `${maxClassCount * 180}px`; relatedProjects.forEach(lp => { // Get original class names from annotationCounts const classNames = Object.keys(lp.annotationCounts || {}); const card = document.createElement('div'); card.className = 'card'; card.style.margin = '18px 0'; card.style.padding = '18px'; card.style.borderRadius = '12px'; card.style.background = '#f5f5f5'; card.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)'; // Extra div for project name const nameDiv = document.createElement('div'); nameDiv.textContent = lp.title || lp.project_id; nameDiv.style.fontSize = '1.2em'; nameDiv.style.fontWeight = 'bold'; nameDiv.style.marginBottom = '12px'; nameDiv.style.background = '#eaf7fa'; nameDiv.style.padding = '8px 16px'; nameDiv.style.borderRadius = '8px'; nameDiv.style.width = projectNameWidth; nameDiv.style.minWidth = projectNameWidth; nameDiv.style.maxWidth = projectNameWidth; nameDiv.style.display = 'inline-block'; card.appendChild(nameDiv); // Container for class rows const classRowsDiv = document.createElement('div'); classRowsDiv.style.display = 'inline-block'; classRowsDiv.style.verticalAlign = 'top'; classRowsDiv.style.width = classRowsWidth; classNames.forEach(className => { // Row for class name and dropdown const row = document.createElement('div'); row.className = 'class-row'; // Mark as class row row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.marginBottom = '10px'; // Original class name const labelSpan = document.createElement('span'); labelSpan.textContent = className; labelSpan.style.fontWeight = 'bold'; labelSpan.style.marginRight = '16px'; labelSpan.style.width = labelWidth; labelSpan.style.minWidth = labelWidth; labelSpan.style.maxWidth = labelWidth; labelSpan.style.display = 'inline-block'; // Dropdown for reassigning const select = document.createElement('select'); select.style.marginLeft = '8px'; select.style.padding = '4px 8px'; select.style.borderRadius = '6px'; select.style.border = '1px solid #009eac'; // Add blank item const blankOption = document.createElement('option'); blankOption.value = ''; blankOption.textContent = ''; select.appendChild(blankOption); storedClasses.forEach(cls => { const option = document.createElement('option'); option.value = cls; option.textContent = cls; select.appendChild(option); }); row.appendChild(labelSpan); row.appendChild(select); classRowsDiv.appendChild(row); }); card.appendChild(classRowsDiv); // Description field (right side, last element) const descDiv = document.createElement('div'); descDiv.className = 'card-description'; descDiv.style.flex = '1'; descDiv.style.marginLeft = '32px'; descDiv.style.display = 'flex'; descDiv.style.flexDirection = 'column'; descDiv.style.justifyContent = 'flex-start'; descDiv.style.alignItems = 'flex-start'; descDiv.style.width = '220px'; // Add a label and textarea for description const descLabel = document.createElement('label'); descLabel.textContent = 'Description:'; descLabel.style.fontWeight = 'bold'; descLabel.style.marginBottom = '4px'; const descTextarea = document.createElement('textarea'); descTextarea.style.width = '220px'; descTextarea.style.height = '48px'; descTextarea.style.borderRadius = '6px'; descTextarea.style.border = '1px solid #009eac'; descTextarea.style.padding = '6px'; descTextarea.style.resize = 'none'; descTextarea.value = lp.description || ''; descDiv.appendChild(descLabel); descDiv.appendChild(descTextarea); card.appendChild(descDiv); detailsDiv.appendChild(card); }); // Add Next button at the bottom right of the page const nextBtn = document.createElement('button'); nextBtn.id = 'next-btn'; nextBtn.className = 'button'; nextBtn.textContent = 'Next'; nextBtn.style.position = 'fixed'; nextBtn.style.right = '32px'; nextBtn.style.bottom = '32px'; nextBtn.style.zIndex = '1000'; document.body.appendChild(nextBtn); // Next button click handler: collect class mappings and update TrainingProjectDetails nextBtn.addEventListener('click', () => { // Array of arrays: [[labelStudioProjectId, [[originalClass, mappedClass], ...]], ...] const mappings = []; const descriptions = []; detailsDiv.querySelectorAll('.card').forEach((card, idx) => { const projectId = relatedProjects[idx].project_id; const classMap = []; // Only iterate over actual class rows card.querySelectorAll('.class-row').forEach(row => { const labelSpan = row.querySelector('span'); const select = row.querySelector('select'); if (labelSpan && select) { const className = labelSpan.textContent.trim(); const mappedValue = select.value.trim(); if (className !== '' && mappedValue !== '') { classMap.push([className, mappedValue]); } } }); mappings.push([projectId, classMap]); // Get description from textarea const descTextarea = card.querySelector('textarea'); descriptions.push([projectId, descTextarea ? descTextarea.value : '']); }); // Update TrainingProjectDetails in DB fetch('/api/training-project-details', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: Number(trainingProjectId), class_map: mappings, description: descriptions // array of [projectId, description] }) }) .then(res => res.json()) .then(data => { alert('Class assignments and descriptions updated!'); console.log(data); // Redirect to start-training.html with id window.location.href = `/edit-training.html?id=${trainingProjectId}`; }) .catch(err => { alert('Error updating class assignments or descriptions'); console.error(err); }); }); }); });