training fix. add global settings

This commit is contained in:
2025-12-02 09:31:52 +01:00
parent 55b1b2b5fe
commit c3c7e042bb
86 changed files with 77512 additions and 7054 deletions

View File

@@ -1,154 +1,154 @@
export function addClass() {
const input_class = document.querySelector('.add-category input.div-wrapper');
let existingClasses;
const input_project_name = document.getElementById('project_name_input')
const description = document.getElementById('project_description_input');
const button_addClass = document.querySelector('.add-category .upload-button-text-wrapper');
const button_addProject = document.querySelector('.popup .confirm-button-datasetcreation')
const classWrapper = document.querySelector('.add-class-wrapper');
button_addProject.addEventListener('click', () => {
const title = input_project_name.value.trim();
const descriptionText = description.value.trim();
const classes = Array.from(classWrapper.querySelectorAll('.overlap-group')).map(el => el.textContent.trim());
const formData = new FormData();
formData.append('title', title);
formData.append('description', descriptionText);
formData.append('classes', JSON.stringify(classes));
if (imgBlob) {
formData.append('project_image', imgBlob, 'project_image.png'); // or the correct file type
}
fetch('/api/training-projects', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
alert(data.message || 'Project created!');
window.location.href = '/index.html';
})
.catch(err => alert('Error: ' + err));
});
button_addClass.addEventListener('click', () => {
const className = input_class.value.trim();
if (!className) {
alert('Please enter a class name');
return;
}
existingClasses = classWrapper.querySelectorAll('.overlap-group');
for (const el of existingClasses) {
if (el.textContent.trim().toLowerCase() === className.toLowerCase()) {
alert(`Class name "${className}" already exists.`);
return;
}
}
const newClassDiv = document.createElement('div');
newClassDiv.classList.add('add-class');
newClassDiv.style.position = 'relative';
newClassDiv.style.width = '335px';
newClassDiv.style.height = '25px';
newClassDiv.style.marginBottom = '5px';
const overlapGroup = document.createElement('div');
overlapGroup.classList.add('overlap-group');
overlapGroup.style.position = 'absolute';
overlapGroup.style.width = '275px';
overlapGroup.style.height = '25px';
overlapGroup.style.top = '0';
overlapGroup.style.left = '0';
overlapGroup.style.backgroundColor = '#30bffc80';
overlapGroup.style.borderRadius = '5px';
overlapGroup.style.display = 'flex';
overlapGroup.style.alignItems = 'center';
overlapGroup.style.paddingLeft = '10px';
overlapGroup.style.color = '#000';
overlapGroup.style.fontFamily = 'var(--m3-body-small-font-family)';
overlapGroup.style.fontWeight = 'var(--m3-body-small-font-weight)';
overlapGroup.style.fontSize = 'var(--m3-body-small-font-size)';
overlapGroup.style.letterSpacing = 'var(--m3-body-small-letter-spacing)';
overlapGroup.style.lineHeight = 'var(--m3-body-small-line-height)';
overlapGroup.textContent = className;
const overlap = document.createElement('div');
overlap.classList.add('overlap');
overlap.style.position = 'absolute';
overlap.style.width = '50px';
overlap.style.height = '25px';
overlap.style.top = '0';
overlap.style.left = '285px';
const rectangle = document.createElement('div');
rectangle.classList.add('rectangle');
rectangle.style.width = '50px';
rectangle.style.height = '25px';
rectangle.style.backgroundColor = '#ff0f43';
rectangle.style.borderRadius = '5px';
rectangle.style.display = 'flex';
rectangle.style.alignItems = 'center';
rectangle.style.justifyContent = 'center';
rectangle.style.cursor = 'pointer';
rectangle.addEventListener('mouseenter', () => {
rectangle.style.backgroundColor = '#bb032b';
});
rectangle.addEventListener('mouseleave', () => {
rectangle.style.backgroundColor = '#ff0f43';
});
const minusText = document.createElement('div');
minusText.classList.add('text-wrapper-4');
minusText.style.position = 'absolute';
minusText.style.top = '-18px';
minusText.style.left = '18px';
minusText.style.fontFamily = 'var(--m3-display-large-font-family)';
minusText.style.fontWeight = 'var(--m3-display-large-font-weight)';
minusText.style.color = '#000000';
minusText.style.fontSize = 'var(--minus-for-button-size)';
minusText.style.letterSpacing = 'var(--m3-display-large-letter-spacing)';
minusText.style.lineHeight = 'var(--m3-display-large-line-height)';
minusText.style.whiteSpace = 'nowrap';
minusText.style.cursor = 'pointer';
minusText.style.fontStyle = 'var(--m3-display-large-font-style)';
minusText.textContent = '_';
rectangle.appendChild(minusText);
rectangle.addEventListener('click', () => {
classWrapper.removeChild(newClassDiv);
document.dispatchEvent(new CustomEvent('classListUpdated'));
});
overlap.appendChild(rectangle);
newClassDiv.appendChild(overlapGroup);
newClassDiv.appendChild(overlap);
classWrapper.appendChild(newClassDiv);
input_class.value = '';
});
}
export function addClass() {
const input_class = document.querySelector('.add-category input.div-wrapper');
let existingClasses;
const input_project_name = document.getElementById('project_name_input')
const description = document.getElementById('project_description_input');
const button_addClass = document.querySelector('.add-category .upload-button-text-wrapper');
const button_addProject = document.querySelector('.popup .confirm-button-datasetcreation')
const classWrapper = document.querySelector('.add-class-wrapper');
button_addProject.addEventListener('click', () => {
const title = input_project_name.value.trim();
const descriptionText = description.value.trim();
const classes = Array.from(classWrapper.querySelectorAll('.overlap-group')).map(el => el.textContent.trim());
const formData = new FormData();
formData.append('title', title);
formData.append('description', descriptionText);
formData.append('classes', JSON.stringify(classes));
if (imgBlob) {
formData.append('project_image', imgBlob, 'project_image.png'); // or the correct file type
}
fetch('/api/training-projects', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
alert(data.message || 'Project created!');
window.location.href = '/index.html';
})
.catch(err => alert('Error: ' + err));
});
button_addClass.addEventListener('click', () => {
const className = input_class.value.trim();
if (!className) {
alert('Please enter a class name');
return;
}
existingClasses = classWrapper.querySelectorAll('.overlap-group');
for (const el of existingClasses) {
if (el.textContent.trim().toLowerCase() === className.toLowerCase()) {
alert(`Class name "${className}" already exists.`);
return;
}
}
const newClassDiv = document.createElement('div');
newClassDiv.classList.add('add-class');
newClassDiv.style.position = 'relative';
newClassDiv.style.width = '335px';
newClassDiv.style.height = '25px';
newClassDiv.style.marginBottom = '5px';
const overlapGroup = document.createElement('div');
overlapGroup.classList.add('overlap-group');
overlapGroup.style.position = 'absolute';
overlapGroup.style.width = '275px';
overlapGroup.style.height = '25px';
overlapGroup.style.top = '0';
overlapGroup.style.left = '0';
overlapGroup.style.backgroundColor = '#30bffc80';
overlapGroup.style.borderRadius = '5px';
overlapGroup.style.display = 'flex';
overlapGroup.style.alignItems = 'center';
overlapGroup.style.paddingLeft = '10px';
overlapGroup.style.color = '#000';
overlapGroup.style.fontFamily = 'var(--m3-body-small-font-family)';
overlapGroup.style.fontWeight = 'var(--m3-body-small-font-weight)';
overlapGroup.style.fontSize = 'var(--m3-body-small-font-size)';
overlapGroup.style.letterSpacing = 'var(--m3-body-small-letter-spacing)';
overlapGroup.style.lineHeight = 'var(--m3-body-small-line-height)';
overlapGroup.textContent = className;
const overlap = document.createElement('div');
overlap.classList.add('overlap');
overlap.style.position = 'absolute';
overlap.style.width = '50px';
overlap.style.height = '25px';
overlap.style.top = '0';
overlap.style.left = '285px';
const rectangle = document.createElement('div');
rectangle.classList.add('rectangle');
rectangle.style.width = '50px';
rectangle.style.height = '25px';
rectangle.style.backgroundColor = '#ff0f43';
rectangle.style.borderRadius = '5px';
rectangle.style.display = 'flex';
rectangle.style.alignItems = 'center';
rectangle.style.justifyContent = 'center';
rectangle.style.cursor = 'pointer';
rectangle.addEventListener('mouseenter', () => {
rectangle.style.backgroundColor = '#bb032b';
});
rectangle.addEventListener('mouseleave', () => {
rectangle.style.backgroundColor = '#ff0f43';
});
const minusText = document.createElement('div');
minusText.classList.add('text-wrapper-4');
minusText.style.position = 'absolute';
minusText.style.top = '-18px';
minusText.style.left = '18px';
minusText.style.fontFamily = 'var(--m3-display-large-font-family)';
minusText.style.fontWeight = 'var(--m3-display-large-font-weight)';
minusText.style.color = '#000000';
minusText.style.fontSize = 'var(--minus-for-button-size)';
minusText.style.letterSpacing = 'var(--m3-display-large-letter-spacing)';
minusText.style.lineHeight = 'var(--m3-display-large-line-height)';
minusText.style.whiteSpace = 'nowrap';
minusText.style.cursor = 'pointer';
minusText.style.fontStyle = 'var(--m3-display-large-font-style)';
minusText.textContent = '_';
rectangle.appendChild(minusText);
rectangle.addEventListener('click', () => {
classWrapper.removeChild(newClassDiv);
document.dispatchEvent(new CustomEvent('classListUpdated'));
});
overlap.appendChild(rectangle);
newClassDiv.appendChild(overlapGroup);
newClassDiv.appendChild(overlap);
classWrapper.appendChild(newClassDiv);
input_class.value = '';
});
}

View File

@@ -1,38 +1,38 @@
//Global Variable
var imgBlob;
var imgMimeType
// Create a hidden file input dynamically
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
function uploadButtonHandler() {
fileInput.click();
};
fileInput.addEventListener('change', () => {
const imageDiv = document.querySelector('.popup .image');
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
imageDiv.innerHTML = ''; // clear previous content
const img = document.createElement('img');
img.src = e.target.result;
img.alt = 'Uploaded Image';
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'cover';
img.style.borderRadius = '10px';
imageDiv.appendChild(img);
// Use the original file as the blob and store its MIME type
imgBlob = file;
imgMimeType = file.type;
};
reader.readAsDataURL(file);
});
//Global Variable
var imgBlob;
var imgMimeType
// Create a hidden file input dynamically
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
function uploadButtonHandler() {
fileInput.click();
};
fileInput.addEventListener('change', () => {
const imageDiv = document.querySelector('.popup .image');
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
imageDiv.innerHTML = ''; // clear previous content
const img = document.createElement('img');
img.src = e.target.result;
img.alt = 'Uploaded Image';
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'cover';
img.style.borderRadius = '10px';
imageDiv.appendChild(img);
// Use the original file as the blob and store its MIME type
imgBlob = file;
imgMimeType = file.type;
};
reader.readAsDataURL(file);
});

View File

@@ -1,137 +1,137 @@
// Fetch LabelStudioProjects from backend and render as selectable cards
window.addEventListener('DOMContentLoaded', () => {
let projectsList = document.getElementById('projects-list');
const selectedIds = new Set();
if (!projectsList) {
// Try to create the container if missing
projectsList = document.createElement('div');
projectsList.id = 'projects-list';
document.body.appendChild(projectsList);
}
else{console.log("noep")}
fetch('/api/label-studio-projects')
.then(res => res.json())
.then(projects => {
projectsList.innerHTML = '';
if (!projects || projects.length === 0) {
projectsList.innerHTML = '<div>No Label Studio projects found</div>';
return;
}
for (const project of projects) {
// Only show card if there is at least one non-empty annotation class
const annotationClasses = Object.entries(project.annotationCounts || {})
.filter(([label, count]) => label && label.trim() !== '');
if (annotationClasses.length === 0) continue;
const card = document.createElement('div');
card.className = 'card';
card.style.background = '#f5f5f5';
card.style.borderRadius = '12px';
card.style.overflow = 'hidden';
card.style.boxShadow = '0 2px 8px rgba(0,0,0,0)';
card.style.display = 'flex';
card.style.background = 'white';
card.style.cursor = 'pointer';
card.tabIndex = 0;
card.setAttribute('role', 'button');
card.setAttribute('aria-label', `Open project ${project.title || project.project_id}`);
// Selection logic
card.dataset.projectId = project.project_id;
card.addEventListener('click', () => {
card.classList.toggle('selected');
if (card.classList.contains('selected')) {
card.style.background = '#009eac'; // main dif color for card
selectedIds.add(project.project_id);
} else {
card.style.background = 'white'; // revert card color
selectedIds.delete(project.project_id);
}
// Debug: log selected ids array
console.log(Array.from(selectedIds));
});
// Info
const infoDiv = document.createElement('div');
infoDiv.className = 'info';
infoDiv.style.background = 'rgba(210, 238, 240)';
infoDiv.style.flex = '1';
infoDiv.style.padding = '16px';
infoDiv.innerHTML = `
<h3 style="margin:0 0 8px 0;font-size:1.5em;font-weight:bold;">${project.project_id ?? 'N/A'} &nbsp;&nbsp;&nbsp; ${project.title || 'Untitled'}</h3>
<div class="label-classes" style="font-size:1em;">
${annotationClasses.map(([label, count]) => `<p>${label}: ${count}</p>`).join('')}
</div>
`;
card.appendChild(infoDiv);
projectsList.appendChild(card);
}
})
.catch(() => {
projectsList.innerHTML = '<div>Error loading Label Studio projects</div>';
});
// 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);
// Get training_project_id from URL
const urlParams = new URLSearchParams(window.location.search);
const trainingProjectId = urlParams.get('id');
// Next button click handler
nextBtn.addEventListener('click', () => {
console.log(trainingProjectId)
if (!trainingProjectId) {
alert('No training project selected.');
return;
}
if (selectedIds.size === 0) {
alert('Please select at least one Label Studio project.');
return;
}
const annotationProjectsJson = JSON.stringify(Array.from(selectedIds));
fetch('/api/training-project-details', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
project_id: Number(trainingProjectId),
annotation_projects: Array.from(selectedIds)
})
})
.then(res => res.json())
.then(data => {
alert('TrainingProjectDetails saved!');
console.log(data);
// Redirect to start-training.html with id
window.location.href = `/setup-training-project.html?id=${trainingProjectId}`;
})
.catch(err => {
alert('Error saving TrainingProjectDetails');
console.error(err);
});
});
// Add description field above the project cards
const descDiv = document.createElement('div');
descDiv.id = 'dashboard-description';
descDiv.style.width = '100%';
descDiv.style.maxWidth = '900px';
descDiv.style.margin = '0 auto 24px auto';
descDiv.style.padding = '18px 24px';
descDiv.style.background = '#eaf7fa';
descDiv.style.borderRadius = '12px';
descDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)';
descDiv.style.fontSize = '1.15em';
descDiv.style.color = '#009eac';
descDiv.style.textAlign = 'center';
descDiv.textContent = 'Select one or more Label Studio projects by clicking the cards below. The annotation summary for each project is shown. Click Next to continue.';
projectsList.parentNode.insertBefore(descDiv, projectsList);
});
// Fetch LabelStudioProjects from backend and render as selectable cards
window.addEventListener('DOMContentLoaded', () => {
let projectsList = document.getElementById('projects-list');
const selectedIds = new Set();
if (!projectsList) {
// Try to create the container if missing
projectsList = document.createElement('div');
projectsList.id = 'projects-list';
document.body.appendChild(projectsList);
}
else{console.log("noep")}
fetch('/api/label-studio-projects')
.then(res => res.json())
.then(projects => {
projectsList.innerHTML = '';
if (!projects || projects.length === 0) {
projectsList.innerHTML = '<div>No Label Studio projects found</div>';
return;
}
for (const project of projects) {
// Only show card if there is at least one non-empty annotation class
const annotationClasses = Object.entries(project.annotationCounts || {})
.filter(([label, count]) => label && label.trim() !== '');
if (annotationClasses.length === 0) continue;
const card = document.createElement('div');
card.className = 'card';
card.style.background = '#f5f5f5';
card.style.borderRadius = '12px';
card.style.overflow = 'hidden';
card.style.boxShadow = '0 2px 8px rgba(0,0,0,0)';
card.style.display = 'flex';
card.style.background = 'white';
card.style.cursor = 'pointer';
card.tabIndex = 0;
card.setAttribute('role', 'button');
card.setAttribute('aria-label', `Open project ${project.title || project.project_id}`);
// Selection logic
card.dataset.projectId = project.project_id;
card.addEventListener('click', () => {
card.classList.toggle('selected');
if (card.classList.contains('selected')) {
card.style.background = '#009eac'; // main dif color for card
selectedIds.add(project.project_id);
} else {
card.style.background = 'white'; // revert card color
selectedIds.delete(project.project_id);
}
// Debug: log selected ids array
console.log(Array.from(selectedIds));
});
// Info
const infoDiv = document.createElement('div');
infoDiv.className = 'info';
infoDiv.style.background = 'rgba(210, 238, 240)';
infoDiv.style.flex = '1';
infoDiv.style.padding = '16px';
infoDiv.innerHTML = `
<h3 style="margin:0 0 8px 0;font-size:1.5em;font-weight:bold;">${project.project_id ?? 'N/A'} &nbsp;&nbsp;&nbsp; ${project.title || 'Untitled'}</h3>
<div class="label-classes" style="font-size:1em;">
${annotationClasses.map(([label, count]) => `<p>${label}: ${count}</p>`).join('')}
</div>
`;
card.appendChild(infoDiv);
projectsList.appendChild(card);
}
})
.catch(() => {
projectsList.innerHTML = '<div>Error loading Label Studio projects</div>';
});
// 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);
// Get training_project_id from URL
const urlParams = new URLSearchParams(window.location.search);
const trainingProjectId = urlParams.get('id');
// Next button click handler
nextBtn.addEventListener('click', () => {
console.log(trainingProjectId)
if (!trainingProjectId) {
alert('No training project selected.');
return;
}
if (selectedIds.size === 0) {
alert('Please select at least one Label Studio project.');
return;
}
const annotationProjectsJson = JSON.stringify(Array.from(selectedIds));
fetch('/api/training-project-details', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
project_id: Number(trainingProjectId),
annotation_projects: Array.from(selectedIds)
})
})
.then(res => res.json())
.then(data => {
alert('TrainingProjectDetails saved!');
console.log(data);
// Redirect to start-training.html with id
window.location.href = `/setup-training-project.html?id=${trainingProjectId}`;
})
.catch(err => {
alert('Error saving TrainingProjectDetails');
console.error(err);
});
});
// Add description field above the project cards
const descDiv = document.createElement('div');
descDiv.id = 'dashboard-description';
descDiv.style.width = '100%';
descDiv.style.maxWidth = '900px';
descDiv.style.margin = '0 auto 24px auto';
descDiv.style.padding = '18px 24px';
descDiv.style.background = '#eaf7fa';
descDiv.style.borderRadius = '12px';
descDiv.style.boxShadow = '0 2px 8px rgba(0,0,0,0.04)';
descDiv.style.fontSize = '1.15em';
descDiv.style.color = '#009eac';
descDiv.style.textAlign = 'center';
descDiv.textContent = 'Select one or more Label Studio projects by clicking the cards below. The annotation summary for each project is shown. Click Next to continue.';
projectsList.parentNode.insertBefore(descDiv, projectsList);
});

View File

@@ -1,171 +1,171 @@
function renderProjects(projects) {
const projectsList = document.getElementById('projects-list');
projectsList.innerHTML = '';
if (projects.length === 0) {
projectsList.innerHTML = '<div>No projects found</div>';
return;
}
for (const project of projects) {
const labelCounts = project.labelCounts || {};
const card = document.createElement('div');
card.className = 'card';
card.style.background = '#f5f5f5';
card.style.borderRadius = '12px';
card.style.overflow = 'hidden';
card.style.boxShadow = '0 2px 8px rgba(0,0,0,0)';
card.style.display = 'flex';
card.style.background = 'white';
card.style.cursor = 'pointer';
card.tabIndex = 0;
card.setAttribute('role', 'button');
card.setAttribute('aria-label', `Open project ${project.title || project.id}`);
card.style.position = 'relative'; // For absolute positioning of delete button
card.addEventListener('click', (e) => {
// Prevent click if delete button is pressed
if (e.target.classList.contains('delete-btn')) return;
if (project.hasTraining) {
window.location.href = `/overview-training.html?id=${project.id}`;
} else if (project.hasDetails) {
// Find details for this project
const detailsEntry = window._trainingProjectDetails?.find(d => d.project_id == project.id);
if (detailsEntry && Array.isArray(detailsEntry.class_map) && detailsEntry.class_map.length > 0) {
// If classes are assigned, skip to start-training.html
window.location.href = `/edit-training.html?id=${project.id}`;
} else {
window.location.href = `/setup-training-project.html?id=${project.id}`;
}
} else {
window.location.href = `/project-details.html?id=${project.id}`;
}
});
// Image
let imageHTML = '';
if (project.project_image) {
imageHTML = `<img src="${project.project_image}" alt="img" style="width:120px;height:120px;object-fit:cover;display:block;" />`;
}
const imgContainer = document.createElement('div');
imgContainer.className = 'img-container';
imgContainer.style.background = '#009eac2d'
imgContainer.style.flex = '0 0 120px';
imgContainer.style.display = 'flex';
imgContainer.style.alignItems = 'center';
imgContainer.style.justifyContent = 'center';
imgContainer.innerHTML = imageHTML;
// Info
const infoDiv = document.createElement('div');
infoDiv.className = 'info';
infoDiv.style.background = '#009eac2d'
infoDiv.style.flex = '1';
infoDiv.style.padding = '16px';
infoDiv.innerHTML = `
<h3 style="margin:0 0 8px 0;font-size:1.5em;font-weight:bold;">${project.id ?? 'N/A'} &nbsp&nbsp&nbsp ${project.title || 'Untitled'}</h3>
<div class="label-classes" style="font-size:1em;">
${getClassesAsParagraphs(project, labelCounts)}
</div>
`;
// Delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.style.width = '70px';
deleteBtn.style.height = '28px';
deleteBtn.className = 'button-red delete-btn';
deleteBtn.style.position = 'absolute';
deleteBtn.style.bottom = '0px';
deleteBtn.style.right = '15px';
deleteBtn.style.zIndex = '2';
deleteBtn.style.fontSize = '14px';
deleteBtn.style.padding = '0';
deleteBtn.style.borderRadius = '6px';
deleteBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)';
deleteBtn.style.display = 'flex';
deleteBtn.style.alignItems = 'center';
deleteBtn.style.justifyContent = 'center';
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (confirm('Are you sure you want to delete this training project?')) {
fetch(`/api/training-projects/${project.id}`, { method: 'DELETE' })
.then(res => {
if (res.ok) {
card.remove();
} else {
alert('Failed to delete project.');
}
})
.catch(() => alert('Failed to delete project.'));
}
});
card.appendChild(imgContainer);
card.appendChild(infoDiv);
card.appendChild(deleteBtn);
projectsList.appendChild(card);
}
}
// Helper to render classes as <p> elements
function getClassesAsParagraphs(project, labelCounts) {
let classes = [];
let labelConfig = project.parsed_label_config;
if (typeof labelConfig === 'string') {
try { labelConfig = JSON.parse(labelConfig); } catch { labelConfig = null; }
}
if (labelConfig) {
Object.values(labelConfig).forEach(cfg => {
if (cfg.labels && Array.isArray(cfg.labels)) {
cfg.labels.forEach(label => {
classes.push(label);
});
}
});
}
if (classes.length === 0 && project.prompts && project.prompts.length > 0) {
const prompt = project.prompts[0];
if (prompt.output_classes && prompt.output_classes.length > 0) {
classes = prompt.output_classes;
}
}
if (classes.length === 0 && Object.keys(labelCounts).length > 0) {
classes = Object.keys(labelCounts);
}
return classes.map(cls => `<p>${cls}${labelCounts && labelCounts[cls] !== undefined ? ' ' : ''}</p>`).join('');
}
// Fetch and render TrainingProjects from the backend
window.addEventListener('DOMContentLoaded', () => {
Promise.all([
fetch('/api/training-projects').then(res => res.json()),
fetch('/api/training-project-details').then(res => res.json()),
fetch('/api/trainings').then(res => res.json())
]).then(([projects, details, trainings]) => {
window._trainingProjectDetails = details; // Store globally for click handler
// Build a set of project IDs that have details
const detailsProjectIds = new Set(details.map(d => d.project_id));
// Build a set of project IDs that have trainings
const detailsIdToProjectId = {};
details.forEach(d => { detailsIdToProjectId[d.id] = d.project_id; });
const trainingProjectIds = new Set(trainings.map(t => detailsIdToProjectId[t.project_details_id]));
// Map project_id to id for frontend compatibility
projects.forEach(project => {
if (project.project_id !== undefined) project.id = project.project_id;
if (Array.isArray(project.classes)) {
project.labelCounts = {};
project.classes.forEach(cls => project.labelCounts[cls] = 0);
}
// Attach a flag for details existence
project.hasDetails = detailsProjectIds.has(project.id);
// Attach a flag for training existence
project.hasTraining = trainingProjectIds.has(project.id);
});
renderProjects(projects);
}).catch(err => {
document.getElementById('projects-list').innerHTML = '<div>Error loading projects</div>';
});
});
function renderProjects(projects) {
const projectsList = document.getElementById('projects-list');
projectsList.innerHTML = '';
if (projects.length === 0) {
projectsList.innerHTML = '<div>No projects found</div>';
return;
}
for (const project of projects) {
const labelCounts = project.labelCounts || {};
const card = document.createElement('div');
card.className = 'card';
card.style.background = '#f5f5f5';
card.style.borderRadius = '12px';
card.style.overflow = 'hidden';
card.style.boxShadow = '0 2px 8px rgba(0,0,0,0)';
card.style.display = 'flex';
card.style.background = 'white';
card.style.cursor = 'pointer';
card.tabIndex = 0;
card.setAttribute('role', 'button');
card.setAttribute('aria-label', `Open project ${project.title || project.id}`);
card.style.position = 'relative'; // For absolute positioning of delete button
card.addEventListener('click', (e) => {
// Prevent click if delete button is pressed
if (e.target.classList.contains('delete-btn')) return;
if (project.hasTraining) {
window.location.href = `/overview-training.html?id=${project.id}`;
} else if (project.hasDetails) {
// Find details for this project
const detailsEntry = window._trainingProjectDetails?.find(d => d.project_id == project.id);
if (detailsEntry && Array.isArray(detailsEntry.class_map) && detailsEntry.class_map.length > 0) {
// If classes are assigned, skip to start-training.html
window.location.href = `/edit-training.html?id=${project.id}`;
} else {
window.location.href = `/setup-training-project.html?id=${project.id}`;
}
} else {
window.location.href = `/project-details.html?id=${project.id}`;
}
});
// Image
let imageHTML = '';
if (project.project_image) {
imageHTML = `<img src="${project.project_image}" alt="img" style="width:120px;height:120px;object-fit:cover;display:block;" />`;
}
const imgContainer = document.createElement('div');
imgContainer.className = 'img-container';
imgContainer.style.background = '#009eac2d'
imgContainer.style.flex = '0 0 120px';
imgContainer.style.display = 'flex';
imgContainer.style.alignItems = 'center';
imgContainer.style.justifyContent = 'center';
imgContainer.innerHTML = imageHTML;
// Info
const infoDiv = document.createElement('div');
infoDiv.className = 'info';
infoDiv.style.background = '#009eac2d'
infoDiv.style.flex = '1';
infoDiv.style.padding = '16px';
infoDiv.innerHTML = `
<h3 style="margin:0 0 8px 0;font-size:1.5em;font-weight:bold;">${project.id ?? 'N/A'} &nbsp&nbsp&nbsp ${project.title || 'Untitled'}</h3>
<div class="label-classes" style="font-size:1em;">
${getClassesAsParagraphs(project, labelCounts)}
</div>
`;
// Delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.style.width = '70px';
deleteBtn.style.height = '28px';
deleteBtn.className = 'button-red delete-btn';
deleteBtn.style.position = 'absolute';
deleteBtn.style.bottom = '0px';
deleteBtn.style.right = '15px';
deleteBtn.style.zIndex = '2';
deleteBtn.style.fontSize = '14px';
deleteBtn.style.padding = '0';
deleteBtn.style.borderRadius = '6px';
deleteBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)';
deleteBtn.style.display = 'flex';
deleteBtn.style.alignItems = 'center';
deleteBtn.style.justifyContent = 'center';
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (confirm('Are you sure you want to delete this training project?')) {
fetch(`/api/training-projects/${project.id}`, { method: 'DELETE' })
.then(res => {
if (res.ok) {
card.remove();
} else {
alert('Failed to delete project.');
}
})
.catch(() => alert('Failed to delete project.'));
}
});
card.appendChild(imgContainer);
card.appendChild(infoDiv);
card.appendChild(deleteBtn);
projectsList.appendChild(card);
}
}
// Helper to render classes as <p> elements
function getClassesAsParagraphs(project, labelCounts) {
let classes = [];
let labelConfig = project.parsed_label_config;
if (typeof labelConfig === 'string') {
try { labelConfig = JSON.parse(labelConfig); } catch { labelConfig = null; }
}
if (labelConfig) {
Object.values(labelConfig).forEach(cfg => {
if (cfg.labels && Array.isArray(cfg.labels)) {
cfg.labels.forEach(label => {
classes.push(label);
});
}
});
}
if (classes.length === 0 && project.prompts && project.prompts.length > 0) {
const prompt = project.prompts[0];
if (prompt.output_classes && prompt.output_classes.length > 0) {
classes = prompt.output_classes;
}
}
if (classes.length === 0 && Object.keys(labelCounts).length > 0) {
classes = Object.keys(labelCounts);
}
return classes.map(cls => `<p>${cls}${labelCounts && labelCounts[cls] !== undefined ? ' ' : ''}</p>`).join('');
}
// Fetch and render TrainingProjects from the backend
window.addEventListener('DOMContentLoaded', () => {
Promise.all([
fetch('/api/training-projects').then(res => res.json()),
fetch('/api/training-project-details').then(res => res.json()),
fetch('/api/trainings').then(res => res.json())
]).then(([projects, details, trainings]) => {
window._trainingProjectDetails = details; // Store globally for click handler
// Build a set of project IDs that have details
const detailsProjectIds = new Set(details.map(d => d.project_id));
// Build a set of project IDs that have trainings
const detailsIdToProjectId = {};
details.forEach(d => { detailsIdToProjectId[d.id] = d.project_id; });
const trainingProjectIds = new Set(trainings.map(t => detailsIdToProjectId[t.project_details_id]));
// Map project_id to id for frontend compatibility
projects.forEach(project => {
if (project.project_id !== undefined) project.id = project.project_id;
if (Array.isArray(project.classes)) {
project.labelCounts = {};
project.classes.forEach(cls => project.labelCounts[cls] = 0);
}
// Attach a flag for details existence
project.hasDetails = detailsProjectIds.has(project.id);
// Attach a flag for training existence
project.hasTraining = trainingProjectIds.has(project.id);
});
renderProjects(projects);
}).catch(err => {
document.getElementById('projects-list').innerHTML = '<div>Error loading projects</div>';
});
});

224
js/settings.js Normal file
View File

@@ -0,0 +1,224 @@
// Settings Modal Management
// Function to open modal
window.openSettingsModal = function() {
const modal = document.getElementById('settings-modal');
if (modal) {
modal.style.display = 'flex';
loadSettings();
}
};
// Function to close modal
window.closeSettingsModal = function() {
const modal = document.getElementById('settings-modal');
if (modal) {
modal.style.display = 'none';
}
};
// Close modal when clicking outside
window.addEventListener('click', function(event) {
const modal = document.getElementById('settings-modal');
if (event.target === modal) {
window.closeSettingsModal();
}
});
// Load settings when modal opens
async function loadSettings() {
try {
const response = await fetch('/api/settings');
if (!response.ok) {
throw new Error('Failed to load settings');
}
const settings = await response.json();
const settingsMap = {};
settings.forEach(s => {
settingsMap[s.key] = s.value;
});
// Populate fields with correct IDs
const labelstudioUrl = document.getElementById('labelstudio-url');
const labelstudioToken = document.getElementById('labelstudio-token');
const yoloxPathInput = document.getElementById('yolox-path');
const yoloxVenvPathInput = document.getElementById('yolox-venv-path');
const yoloxOutputPathInput = document.getElementById('yolox-output-path');
const yoloxDataDirInput = document.getElementById('yolox-data-dir');
if (labelstudioUrl) labelstudioUrl.value = settingsMap.labelstudio_api_url || '';
if (labelstudioToken) labelstudioToken.value = settingsMap.labelstudio_api_token || '';
if (yoloxPathInput) yoloxPathInput.value = settingsMap.yolox_path || '';
if (yoloxVenvPathInput) yoloxVenvPathInput.value = settingsMap.yolox_venv_path || '';
if (yoloxOutputPathInput) yoloxOutputPathInput.value = settingsMap.yolox_output_path || '';
if (yoloxDataDirInput) yoloxDataDirInput.value = settingsMap.yolox_data_dir || '';
} catch (error) {
console.error('Error loading settings:', error);
const saveStatus = document.getElementById('save-status');
if (saveStatus) {
showMessage(saveStatus, 'Fehler beim Laden: ' + error.message, 'error');
}
}
}
// Helper functions
function showMessage(element, message, type) {
if (element) {
element.textContent = message;
element.className = 'status-message ' + type;
element.style.display = 'block';
}
}
function hideMessage(element) {
if (element) {
element.style.display = 'none';
}
}
// Event listeners - wait for DOM to be ready
document.addEventListener('DOMContentLoaded', function() {
// Test Label Studio connection
const testLabelStudioBtn = document.getElementById('test-labelstudio-btn');
if (testLabelStudioBtn) {
testLabelStudioBtn.addEventListener('click', async () => {
const apiUrl = document.getElementById('labelstudio-url').value.trim();
const apiToken = document.getElementById('labelstudio-token').value.trim();
const labelstudioStatus = document.getElementById('labelstudio-status');
const loader = document.getElementById('test-ls-loader');
if (!apiUrl || !apiToken) {
showMessage(labelstudioStatus, 'Bitte geben Sie URL und Token ein', 'error');
return;
}
testLabelStudioBtn.disabled = true;
if (loader) loader.style.display = 'block';
hideMessage(labelstudioStatus);
try {
const response = await fetch('/api/settings/test/labelstudio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ api_url: apiUrl, api_token: apiToken })
});
const result = await response.json();
if (result.success) {
showMessage(labelstudioStatus, '✓ ' + result.message, 'success');
} else {
showMessage(labelstudioStatus, '✗ ' + result.message, 'error');
}
} catch (error) {
showMessage(labelstudioStatus, '✗ Fehler: ' + error.message, 'error');
} finally {
testLabelStudioBtn.disabled = false;
if (loader) loader.style.display = 'none';
}
});
}
// Test YOLOX path
const testYoloxBtn = document.getElementById('test-yolox-btn');
if (testYoloxBtn) {
testYoloxBtn.addEventListener('click', async () => {
const path = document.getElementById('yolox-path').value.trim();
const venvPath = document.getElementById('yolox-venv-path').value.trim();
const yoloxStatus = document.getElementById('yolox-status');
const loader = document.getElementById('test-yolox-loader');
if (!path) {
showMessage(yoloxStatus, 'Bitte geben Sie einen YOLOX Pfad ein', 'error');
return;
}
testYoloxBtn.disabled = true;
if (loader) loader.style.display = 'block';
hideMessage(yoloxStatus);
try {
const response = await fetch('/api/settings/test/yolox', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
yolox_path: path,
yolox_venv_path: venvPath
})
});
const result = await response.json();
if (result.success) {
showMessage(yoloxStatus, '✓ ' + result.message, 'success');
} else {
showMessage(yoloxStatus, '✗ ' + result.message, 'error');
}
} catch (error) {
showMessage(yoloxStatus, '✗ Fehler: ' + error.message, 'error');
} finally {
testYoloxBtn.disabled = false;
if (loader) loader.style.display = 'none';
}
});
}
// Save settings
const saveSettingsBtn = document.getElementById('save-settings-btn');
if (saveSettingsBtn) {
saveSettingsBtn.addEventListener('click', async () => {
const labelstudioUrl = document.getElementById('labelstudio-url').value.trim();
const labelstudioToken = document.getElementById('labelstudio-token').value.trim();
const yoloxPathValue = document.getElementById('yolox-path').value.trim();
const yoloxVenvPathValue = document.getElementById('yolox-venv-path').value.trim();
const yoloxOutputPathValue = document.getElementById('yolox-output-path').value.trim();
const yoloxDataDirValue = document.getElementById('yolox-data-dir').value.trim();
const saveStatus = document.getElementById('save-status');
// Validation
if (!labelstudioUrl || !labelstudioToken || !yoloxPathValue || !yoloxVenvPathValue || !yoloxOutputPathValue || !yoloxDataDirValue) {
showMessage(saveStatus, 'Bitte füllen Sie alle Felder aus', 'error');
return;
}
const settings = {
labelstudio_api_url: labelstudioUrl,
labelstudio_api_token: labelstudioToken,
yolox_path: yoloxPathValue,
yolox_venv_path: yoloxVenvPathValue,
yolox_output_path: yoloxOutputPathValue,
yolox_data_dir: yoloxDataDirValue
};
saveSettingsBtn.disabled = true;
const originalText = saveSettingsBtn.textContent;
saveSettingsBtn.textContent = 'Speichern...';
hideMessage(saveStatus);
try {
const response = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (!response.ok) {
throw new Error('Failed to save settings');
}
showMessage(saveStatus, '✓ Einstellungen erfolgreich gespeichert!', 'success');
// Close modal after 1.5 seconds
setTimeout(() => {
window.closeSettingsModal();
}, 1500);
} catch (error) {
showMessage(saveStatus, '✗ Fehler beim Speichern: ' + error.message, 'error');
} finally {
saveSettingsBtn.disabled = false;
saveSettingsBtn.textContent = originalText;
}
});
}
});

View File

@@ -1,216 +1,216 @@
// 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);
});
});
});
// 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);
});
});
});
});

View File

@@ -1,272 +1,272 @@
// Render helper descriptions for YOLOX settings and handle form submission
window.addEventListener('DOMContentLoaded', () => {
// Get the form element at the top
const form = document.getElementById('settings-form');
// Base config state
let currentBaseConfig = null;
let baseConfigFields = [];
// Define which fields are protected by base config
const protectedFields = [
'depth', 'width', 'act', 'max_epoch', 'warmup_epochs', 'warmup_lr',
'scheduler', 'no_aug_epochs', 'min_lr_ratio', 'ema', 'weight_decay',
'momentum', 'input_size', 'mosaic_scale', 'test_size', 'enable_mixup',
'mosaic_prob', 'mixup_prob', 'hsv_prob', 'flip_prob', 'degrees',
'translate', 'shear', 'mixup_scale', 'print_interval', 'eval_interval'
];
// Map backend field names to frontend field names
const fieldNameMap = {
'activation': 'act', // Backend uses 'activation', frontend uses 'act'
'nms_thre': 'nmsthre'
};
// Function to load base config for selected model
function loadBaseConfig(modelName) {
if (!modelName) return Promise.resolve(null);
return fetch(`/api/base-config/${modelName}`)
.then(res => {
if (!res.ok) throw new Error('Base config not found');
return res.json();
})
.catch(err => {
console.warn(`Could not load base config for ${modelName}:`, err);
return null;
});
}
// Function to apply base config to form fields
function applyBaseConfig(config, isCocoMode) {
const infoBanner = document.getElementById('base-config-info');
const modelNameSpan = document.getElementById('base-config-model');
if (!config || !isCocoMode) {
// Hide info banner
if (infoBanner) infoBanner.style.display = 'none';
// Remove grey styling and enable all fields
protectedFields.forEach(fieldName => {
const input = form.querySelector(`[name="${fieldName}"]`);
if (input) {
input.disabled = false;
input.style.backgroundColor = '#f8f8f8';
input.style.color = '#333';
input.style.cursor = 'text';
input.title = '';
}
});
baseConfigFields = [];
return;
}
// Show info banner
if (infoBanner) {
infoBanner.style.display = 'block';
const modelName = form.querySelector('[name="select_model"]')?.value || 'selected model';
if (modelNameSpan) modelNameSpan.textContent = modelName;
}
// Apply base config values and grey out fields
baseConfigFields = [];
Object.entries(config).forEach(([key, value]) => {
// Map backend field name to frontend field name if needed
const frontendFieldName = fieldNameMap[key] || key;
if (protectedFields.includes(frontendFieldName)) {
const input = form.querySelector(`[name="${frontendFieldName}"]`);
if (input) {
baseConfigFields.push(frontendFieldName);
// Set value based on type
if (input.type === 'checkbox') {
input.checked = Boolean(value);
} else if (Array.isArray(value)) {
input.value = value.join(',');
} else {
input.value = value;
}
// Grey out and disable
input.disabled = true;
input.style.backgroundColor = '#d3d3d3';
input.style.color = '#666';
input.style.cursor = 'not-allowed';
// Add title tooltip
const modelName = form.querySelector('[name="select_model"]')?.value || 'selected model';
input.title = `Protected by base config for ${modelName}. Switch to "Train from sketch" to customize.`;
}
}
});
console.log(`Applied base config. Protected fields: ${baseConfigFields.join(', ')}`);
}
// Function to update form based on transfer learning mode
function updateTransferLearningMode() {
const transferLearning = document.getElementById('transfer-learning');
const selectModel = document.getElementById('select-model');
const selectModelRow = document.getElementById('select-model-row');
if (!transferLearning || !selectModel) return;
const isCocoMode = transferLearning.value === 'coco';
const isCustomMode = transferLearning.value === 'custom';
const isSketchMode = transferLearning.value === 'sketch';
const modelName = selectModel.value;
// Show/hide select model based on transfer learning mode
if (selectModelRow) {
if (isSketchMode) {
selectModelRow.style.display = 'none';
} else {
selectModelRow.style.display = '';
}
}
if (isCocoMode && modelName) {
// Load and apply base config
loadBaseConfig(modelName).then(config => {
currentBaseConfig = config;
applyBaseConfig(config, true);
});
} else {
// Clear base config
currentBaseConfig = null;
applyBaseConfig(null, false);
}
}
// Listen for changes to transfer learning dropdown
const transferLearningSelect = document.getElementById('transfer-learning');
if (transferLearningSelect) {
transferLearningSelect.addEventListener('change', updateTransferLearningMode);
}
// Listen for changes to model selection
const modelSelect = document.getElementById('select-model');
if (modelSelect) {
modelSelect.addEventListener('change', updateTransferLearningMode);
}
// Initial update on page load
setTimeout(updateTransferLearningMode, 100);
// Auto-set num_classes from training_project classes array
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('id');
if (projectId && form) {
fetch('/api/training-projects')
.then(res => res.json())
.then(projects => {
const project = projects.find(p => p.project_id == projectId || p.id == projectId);
if (project && project.classes) {
let classesArr = project.classes;
// If classes is a stringified JSON, parse it
if (typeof classesArr === 'string') {
try {
classesArr = JSON.parse(classesArr);
} catch (e) {
classesArr = [];
}
}
let numClasses = 0;
if (Array.isArray(classesArr)) {
numClasses = classesArr.length;
} else if (typeof classesArr === 'object' && classesArr !== null) {
numClasses = Object.keys(classesArr).length;
}
// Fix: Only set num_classes if input exists
const numClassesInput = form.querySelector('[name="num_classes"]');
if (numClassesInput) {
numClassesInput.value = numClasses;
numClassesInput.readOnly = true;
numClassesInput.dispatchEvent(new Event('input'));
}
}
});
}
// Handle form submission
form.addEventListener('submit', function(e) {
console.log("Form submitted");
e.preventDefault();
// Temporarily enable disabled fields so they get included in FormData
const disabledInputs = [];
form.querySelectorAll('input[disabled], select[disabled]').forEach(input => {
input.disabled = false;
disabledInputs.push(input);
});
const formData = new FormData(form);
const settings = {};
let fileToUpload = null;
for (const [key, value] of formData.entries()) {
if (key === 'model_upload' && form.elements[key].files.length > 0) {
fileToUpload = form.elements[key].files[0];
continue;
}
if (key === 'ema' || key === 'enable_mixup' || key === 'save_history_ckpt') {
settings[key] = form.elements[key].checked;
} else if (key === 'scale' || key === 'mosaic_scale' || key === 'mixup_scale' || key === 'input_size' || key === 'test_size') {
settings[key] = value.split(',').map(v => parseFloat(v.trim()));
} else if (!isNaN(value) && value !== '') {
settings[key] = parseFloat(value);
} else {
settings[key] = value;
}
}
// Re-disable the inputs
disabledInputs.forEach(input => {
input.disabled = true;
});
// Attach project id from URL
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('id');
if (projectId) settings.project_id = Number(projectId);
// First, send settings JSON (without file)
fetch('/api/yolox-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
})
.then(res => res.json())
.then(data => {
// If file selected, send it as binary
if (fileToUpload) {
const reader = new FileReader();
reader.onload = function(e) {
fetch(`/api/yolox-settings/upload?project_id=${settings.project_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: e.target.result
})
.then(res => res.json())
.then(data2 => {
alert('YOLOX settings and model file saved!');
window.location.href = `/overview-training.html?id=${settings.project_id}`;
})
.catch(err => {
alert('Error uploading model file');
console.error(err);
});
};
reader.readAsArrayBuffer(fileToUpload);
} else {
alert('YOLOX settings saved!');
window.location.href = `/overview-training.html?id=${settings.project_id}`;
}
})
.catch(err => {
alert('Error saving YOLOX settings');
console.error(err);
});
});
});
// Render helper descriptions for YOLOX settings and handle form submission
window.addEventListener('DOMContentLoaded', () => {
// Get the form element at the top
const form = document.getElementById('settings-form');
// Base config state
let currentBaseConfig = null;
let baseConfigFields = [];
// Define which fields are protected by base config
const protectedFields = [
'depth', 'width', 'act', 'max_epoch', 'warmup_epochs', 'warmup_lr',
'scheduler', 'no_aug_epochs', 'min_lr_ratio', 'ema', 'weight_decay',
'momentum', 'input_size', 'mosaic_scale', 'test_size', 'enable_mixup',
'mosaic_prob', 'mixup_prob', 'hsv_prob', 'flip_prob', 'degrees',
'translate', 'shear', 'mixup_scale', 'print_interval', 'eval_interval'
];
// Map backend field names to frontend field names
const fieldNameMap = {
'activation': 'act', // Backend uses 'activation', frontend uses 'act'
'nms_thre': 'nmsthre'
};
// Function to load base config for selected model
function loadBaseConfig(modelName) {
if (!modelName) return Promise.resolve(null);
return fetch(`/api/base-config/${modelName}`)
.then(res => {
if (!res.ok) throw new Error('Base config not found');
return res.json();
})
.catch(err => {
console.warn(`Could not load base config for ${modelName}:`, err);
return null;
});
}
// Function to apply base config to form fields
function applyBaseConfig(config, isCocoMode) {
const infoBanner = document.getElementById('base-config-info');
const modelNameSpan = document.getElementById('base-config-model');
if (!config || !isCocoMode) {
// Hide info banner
if (infoBanner) infoBanner.style.display = 'none';
// Remove grey styling and enable all fields
protectedFields.forEach(fieldName => {
const input = form.querySelector(`[name="${fieldName}"]`);
if (input) {
input.disabled = false;
input.style.backgroundColor = '#f8f8f8';
input.style.color = '#333';
input.style.cursor = 'text';
input.title = '';
}
});
baseConfigFields = [];
return;
}
// Show info banner
if (infoBanner) {
infoBanner.style.display = 'block';
const modelName = form.querySelector('[name="select_model"]')?.value || 'selected model';
if (modelNameSpan) modelNameSpan.textContent = modelName;
}
// Apply base config values and grey out fields
baseConfigFields = [];
Object.entries(config).forEach(([key, value]) => {
// Map backend field name to frontend field name if needed
const frontendFieldName = fieldNameMap[key] || key;
if (protectedFields.includes(frontendFieldName)) {
const input = form.querySelector(`[name="${frontendFieldName}"]`);
if (input) {
baseConfigFields.push(frontendFieldName);
// Set value based on type
if (input.type === 'checkbox') {
input.checked = Boolean(value);
} else if (Array.isArray(value)) {
input.value = value.join(',');
} else {
input.value = value;
}
// Grey out and disable
input.disabled = true;
input.style.backgroundColor = '#d3d3d3';
input.style.color = '#666';
input.style.cursor = 'not-allowed';
// Add title tooltip
const modelName = form.querySelector('[name="select_model"]')?.value || 'selected model';
input.title = `Protected by base config for ${modelName}. Switch to "Train from sketch" to customize.`;
}
}
});
console.log(`Applied base config. Protected fields: ${baseConfigFields.join(', ')}`);
}
// Function to update form based on transfer learning mode
function updateTransferLearningMode() {
const transferLearning = document.getElementById('transfer-learning');
const selectModel = document.getElementById('select-model');
const selectModelRow = document.getElementById('select-model-row');
if (!transferLearning || !selectModel) return;
const isCocoMode = transferLearning.value === 'coco';
const isCustomMode = transferLearning.value === 'custom';
const isSketchMode = transferLearning.value === 'sketch';
const modelName = selectModel.value;
// Show/hide select model based on transfer learning mode
if (selectModelRow) {
if (isSketchMode) {
selectModelRow.style.display = 'none';
} else {
selectModelRow.style.display = '';
}
}
if (isCocoMode && modelName) {
// Load and apply base config
loadBaseConfig(modelName).then(config => {
currentBaseConfig = config;
applyBaseConfig(config, true);
});
} else {
// Clear base config
currentBaseConfig = null;
applyBaseConfig(null, false);
}
}
// Listen for changes to transfer learning dropdown
const transferLearningSelect = document.getElementById('transfer-learning');
if (transferLearningSelect) {
transferLearningSelect.addEventListener('change', updateTransferLearningMode);
}
// Listen for changes to model selection
const modelSelect = document.getElementById('select-model');
if (modelSelect) {
modelSelect.addEventListener('change', updateTransferLearningMode);
}
// Initial update on page load
setTimeout(updateTransferLearningMode, 100);
// Auto-set num_classes from training_project classes array
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('id');
if (projectId && form) {
fetch('/api/training-projects')
.then(res => res.json())
.then(projects => {
const project = projects.find(p => p.project_id == projectId || p.id == projectId);
if (project && project.classes) {
let classesArr = project.classes;
// If classes is a stringified JSON, parse it
if (typeof classesArr === 'string') {
try {
classesArr = JSON.parse(classesArr);
} catch (e) {
classesArr = [];
}
}
let numClasses = 0;
if (Array.isArray(classesArr)) {
numClasses = classesArr.length;
} else if (typeof classesArr === 'object' && classesArr !== null) {
numClasses = Object.keys(classesArr).length;
}
// Fix: Only set num_classes if input exists
const numClassesInput = form.querySelector('[name="num_classes"]');
if (numClassesInput) {
numClassesInput.value = numClasses;
numClassesInput.readOnly = true;
numClassesInput.dispatchEvent(new Event('input'));
}
}
});
}
// Handle form submission
form.addEventListener('submit', function(e) {
console.log("Form submitted");
e.preventDefault();
// Temporarily enable disabled fields so they get included in FormData
const disabledInputs = [];
form.querySelectorAll('input[disabled], select[disabled]').forEach(input => {
input.disabled = false;
disabledInputs.push(input);
});
const formData = new FormData(form);
const settings = {};
let fileToUpload = null;
for (const [key, value] of formData.entries()) {
if (key === 'model_upload' && form.elements[key].files.length > 0) {
fileToUpload = form.elements[key].files[0];
continue;
}
if (key === 'ema' || key === 'enable_mixup' || key === 'save_history_ckpt') {
settings[key] = form.elements[key].checked;
} else if (key === 'scale' || key === 'mosaic_scale' || key === 'mixup_scale' || key === 'input_size' || key === 'test_size') {
settings[key] = value.split(',').map(v => parseFloat(v.trim()));
} else if (!isNaN(value) && value !== '') {
settings[key] = parseFloat(value);
} else {
settings[key] = value;
}
}
// Re-disable the inputs
disabledInputs.forEach(input => {
input.disabled = true;
});
// Attach project id from URL
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('id');
if (projectId) settings.project_id = Number(projectId);
// First, send settings JSON (without file)
fetch('/api/yolox-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
})
.then(res => res.json())
.then(data => {
// If file selected, send it as binary
if (fileToUpload) {
const reader = new FileReader();
reader.onload = function(e) {
fetch(`/api/yolox-settings/upload?project_id=${settings.project_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: e.target.result
})
.then(res => res.json())
.then(data2 => {
alert('YOLOX settings and model file saved!');
window.location.href = `/overview-training.html?id=${settings.project_id}`;
})
.catch(err => {
alert('Error uploading model file');
console.error(err);
});
};
reader.readAsArrayBuffer(fileToUpload);
} else {
alert('YOLOX settings saved!');
window.location.href = `/overview-training.html?id=${settings.project_id}`;
}
})
.catch(err => {
alert('Error saving YOLOX settings');
console.error(err);
});
});
});

View File

@@ -1,11 +1,11 @@
// js/storage.js
export function getStoredProjects() {
try {
return JSON.parse(localStorage.getItem('ls_projects') || '{}');
} catch (e) {
return {};
}
}
export function setStoredProjects(projectsObj) {
localStorage.setItem('ls_projects', JSON.stringify(projectsObj));
// js/storage.js
export function getStoredProjects() {
try {
return JSON.parse(localStorage.getItem('ls_projects') || '{}');
} catch (e) {
return {};
}
}
export function setStoredProjects(projectsObj) {
localStorage.setItem('ls_projects', JSON.stringify(projectsObj));
}