training fix. add global settings
This commit is contained in:
308
js/add-class.js
308
js/add-class.js
@@ -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 = '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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'} ${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'} ${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);
|
||||
});
|
||||
|
||||
342
js/dashboard.js
342
js/dashboard.js
@@ -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'}     ${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'}     ${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
224
js/settings.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user