Skip to content

Commit

Permalink
Added message type indicators to toasts: notice, warning, error
Browse files Browse the repository at this point in the history
  • Loading branch information
Mattk70 committed Oct 7, 2024
1 parent cda38c5 commit 521f6e3
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 42 deletions.
84 changes: 57 additions & 27 deletions js/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -849,14 +849,14 @@ const displayLocationAddress = async (where) => {
latEl = document.getElementById('customLat');
lonEl = document.getElementById('customLon');
placeEl = document.getElementById('customPlace');
address = await fetchLocationAddress(latEl.value, lonEl.value, false).catch(error => console.warn(error));
address = await fetchLocationAddress(latEl.value, lonEl.value, false);
if (address === false) return
placeEl.value = address || 'Location not available';
} else {
latEl = document.getElementById('latitude');
lonEl = document.getElementById('longitude');
placeEl = document.getElementById('place');
address = await fetchLocationAddress(latEl.value, lonEl.value, false).catch(error => console.warn(error));;
address = await fetchLocationAddress(latEl.value, lonEl.value, false);
if (address === false) return
const content = '<span class="material-symbols-outlined">fmd_good</span> ' + address;
placeEl.innerHTML = content;
Expand Down Expand Up @@ -1095,15 +1095,15 @@ function postAnalyseMessage(args) {
circleClicked: args.fromDB
});
} else {
generateToast({message: 'An analysis is underway. Press <b>Esc</b> to cancel it before running a new analysis.'})
generateToast({type: 'warning', message: 'An analysis is underway. Press <b>Esc</b> to cancel it before running a new analysis.'})
}
}

let openStreetMapTimer;
function fetchLocationAddress(lat, lon) {

if (isNaN(lat) || isNaN(lon) || !lat || !lon){
generateToast({ message:'Both lat and lon values need to be numbers between 180 and -180'})
generateToast({type: 'warning', message:'Both lat and lon values need to be numbers between 180 and -180'})
return false
}
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -1199,7 +1199,7 @@ function hideAll() {

async function batchExportAudio() {
const species = isSpeciesViewFiltered(true);
species ? exportData('audio', species, 1000) : generateToast({message: "Filter results by species to export audio files"});
species ? exportData('audio', species, 1000) : generateToast({type: 'warning', message: "Filter results by species to export audio files"});
}

const export2CSV = () => exportData('text', isSpeciesViewFiltered(true), Infinity);
Expand Down Expand Up @@ -1911,7 +1911,7 @@ const setUpWorkerMessaging = () => {
</div>
`
}
generateToast({ message: args.message});
generateToast({ type: args.type, message: args.message});
// This is how we know the database update has completed
if (args.database && config.archive.auto) document.getElementById('compress-and-organise').click();
break;
Expand Down Expand Up @@ -1989,7 +1989,7 @@ const setUpWorkerMessaging = () => {
if (!config.hasNode && config[config.model].backend !== 'webgpu'){
// No node? Not using webgpu? Force webgpu
handleBackendChange('webgpu');
generateToast({ message: 'The standard backend could not be loaded on this machine. An experimental backend (webGPU) has been used instead.'});
generateToast({type: 'warning', message: 'The standard backend could not be loaded on this machine. An experimental backend (webGPU) has been used instead.'});
console.warn('tfjs-node could not be loaded, webGPU backend forced. CPU is', DIAGNOSTICS['CPU'])
}
modelSettingsDisplay();
Expand All @@ -2016,7 +2016,7 @@ const setUpWorkerMessaging = () => {
onWorkerLoadedAudio(args);
break;
}
default: {generateToast({ message:`Unrecognised message from worker:${args.event}`});
default: {generateToast({type: 'error', message:`Unrecognised message from worker:${args.event}`});
}
}
})
Expand Down Expand Up @@ -2953,7 +2953,7 @@ function centreSpec(){
seconds = Math.min(parseFloat(timeArray[2]), 59.999);
} else {
// Invalid input
generateToast({ message:'Invalid time format. Please enter time in one of the following formats: \n1. Float (for seconds) \n2. Two numbers separated by a colon (for minutes and seconds) \n3. Three numbers separated by colons (for hours, minutes, and seconds)'});
generateToast({type: 'warning', message:'Invalid time format. Please enter time in one of the following formats: \n1. Float (for seconds) \n2. Two numbers separated by a colon (for minutes and seconds) \n3. Three numbers separated by colons (for hours, minutes, and seconds)'});
return;
}
let start = hours * 3600 + minutes * 60 + seconds;
Expand Down Expand Up @@ -4539,7 +4539,7 @@ DOM.gain.addEventListener('input', () => {
}
case 'clear-call-cache': {
const data = fs.rm(p.join(appPath, 'XCcache.json'), err =>{
if (err) generateToast({message: 'No call cache was found.'}) && config.debug && console.log('No XC cache found', err);
if (err) generateToast({type: 'error', message: 'No call cache was found.'}) && config.debug && console.log('No XC cache found', err);
else generateToast({message: 'The call cache was successfully cleared.'})
})
break;
Expand Down Expand Up @@ -4576,7 +4576,7 @@ DOM.gain.addEventListener('input', () => {
switch (target) {
case 'species-frequency-threshold' : {
if (isNaN(element.value) || element.value === '') {
generateToast({ message:'The threshold must be a number between 0.001 and 1'});
generateToast({type: 'warning', message:'The threshold must be a number between 0.001 and 1'});
return false
}
config.speciesThreshold = element.value;
Expand Down Expand Up @@ -4631,7 +4631,7 @@ DOM.gain.addEventListener('input', () => {
if (element.value === 'custom'){
labelFile = config.customListFile[config.model];
if (! labelFile) {
generateToast({message: 'You must select a label file in the list settings to use the custom language option.'});
generateToast({type: 'warning', message: 'You must select a label file in the list settings to use the custom language option.'});
return;
}
} else {
Expand Down Expand Up @@ -4826,7 +4826,7 @@ function setListUIState(list){
} else if (list === 'custom') {
DOM.customListContainer.classList.remove('d-none');
if (!config.customListFile[config.model]) {
generateToast({message: 'You need to upload a custom list for the model before using the custom list option.'})
generateToast({type: 'warning', message: 'You need to upload a custom list for the model before using the custom list option.'})
return
}
readLabels(config.customListFile[config.model], 'list');
Expand All @@ -4839,7 +4839,7 @@ async function readLabels(labelFile, updating){
return response.text();
}).catch(error =>{
if (error.message === 'Failed to fetch') {
generateToast({message: 'The custom list could not be found, <b class="text-danger">no detections will be shown</b>.'})
generateToast({type: 'error', message: 'The custom list could not be found, <b class="text-danger">no detections will be shown</b>.'})
DOM.customListSelector.classList.add('btn-outline-danger');
document.getElementById('navbarSettings').click();
document.getElementById('list-file-selector').focus();
Expand Down Expand Up @@ -5258,7 +5258,8 @@ async function readLabels(labelFile, updating){
}
}

function generateToast({message}) {

function generateToast({message = '', type = 'info'} ={}) {
const domEl = document.getElementById('toastContainer');

const wrapper = document.createElement('div');
Expand All @@ -5268,16 +5269,45 @@ async function readLabels(labelFile, updating){
wrapper.setAttribute("aria-live", "assertive");
wrapper.setAttribute("aria-atomic", "true");

wrapper.innerHTML = `
<div class="toast-header">
<svg class="bi flex-shrink-0 me-2 text-primary" width="20" height="20" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
<strong class="me-auto">Notice</strong>
<small class="text-muted">just now</small>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>`
// Create elements
const toastHeader = document.createElement('div');
toastHeader.className = 'toast-header';

const iconSpan = document.createElement('span');
iconSpan.classList.add('material-symbols-outlined', 'pe-2');
iconSpan.textContent = type; // The icon name
const typeColours = { info: 'text-primary', warning: 'text-warning', error: 'text-danger'};
const typeText = { info: 'Notice', warning: 'Warning', error: 'Error'};
iconSpan.classList.add(typeColours[type]);
const strong = document.createElement('strong');
strong.className = 'me-auto';
strong.textContent = typeText[type];

const small = document.createElement('small');
small.className = 'text-muted';
small.textContent = 'just now';

const button = document.createElement('button');
button.type = 'button';
button.className = 'btn-close';
button.setAttribute('data-bs-dismiss', 'toast');
button.setAttribute('aria-label', 'Close');

// Append elements to toastHeader
toastHeader.appendChild(iconSpan);
toastHeader.appendChild(strong);
toastHeader.appendChild(small);
toastHeader.appendChild(button);

// Create toast body
const toastBody = document.createElement('div');
toastBody.className = 'toast-body';
toastBody.textContent = message; // Assuming message is defined

// Append header and body to the wrapper
wrapper.appendChild(toastHeader);
wrapper.appendChild(toastBody);


domEl.appendChild(wrapper)
const toast = new bootstrap.Toast(wrapper)
Expand Down Expand Up @@ -5372,7 +5402,7 @@ async function getXCComparisons(){
.then(response =>{
if (! response.ok) {
loading.classList.add('d-none');
return generateToast({message: 'The Xeno-canto API is not responding'})
return generateToast({type: 'error', message: 'The Xeno-canto API is not responding'})
}
return response.json()
})
Expand Down Expand Up @@ -5423,7 +5453,7 @@ async function getXCComparisons(){
}
});
if (songCount === 0 && callCount === 0 && flightCallCount === 0 && nocturnalFlightCallCount === 0) {
generateToast({message: 'The Xeno-canto site has no comparisons available'})
generateToast({type: 'warning', message: 'The Xeno-canto site has no comparisons available'})
return
} else {
// Let's cache the result, 'cos the XC API is quite slow
Expand Down
30 changes: 15 additions & 15 deletions js/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ console.error = function() {
// Implement error handling in the worker
self.onerror = function(message, file, lineno, colno, error) {
STATE.track && trackEvent(STATE.UUID, 'Unhandled Worker Error', message, customURLEncode(error?.stack));
if (message.includes('dynamic link library')) UI.postMessage({event: 'generate-alert', message: 'There has been an error loading the model. This may be due to missing AVX support. Chirpity AI models require the AVX2 instructions set to run. If you have AVX2 enabled and still see this notice, please refer to <a href="https://github.com/Mattk70/Chirpity-Electron/issues/84" target="_blank">this issue</a> on Github.'})
if (message.includes('dynamic link library')) UI.postMessage({event: 'generate-alert', type: 'error', message: 'There has been an error loading the model. This may be due to missing AVX support. Chirpity AI models require the AVX2 instructions set to run. If you have AVX2 enabled and still see this notice, please refer to <a href="https://github.com/Mattk70/Chirpity-Electron/issues/84" target="_blank">this issue</a> on Github.'})
// Return false not to inhibit the default error handling
return false;
};
Expand Down Expand Up @@ -520,7 +520,7 @@ function savedFileCheck(fileList) {
}
});
} else {
UI.postMessage({event: 'generate-alert', message: 'The database has not finished loading. The saved file check was skipped'})
UI.postMessage({event: 'generate-alert', type: 'error', message: 'The database has not finished loading. The saved file check was skipped'})
return undefined
}
}
Expand Down Expand Up @@ -951,7 +951,7 @@ const getDuration = async (src) => {
resolve(duration);
});
audio.addEventListener('error', (error) => {
UI.postMessage({event: 'generate-alert', message: 'Unable to decode file metatada'})
UI.postMessage({event: 'generate-alert', type: 'error', message: 'Unable to decode file metatada'})
reject(error)
})
});
Expand Down Expand Up @@ -1024,7 +1024,7 @@ async function notifyMissingFile(file) {
const row = await diskDB.getAsync('SELECT * FROM FILES WHERE name = ?', file);
if (row?.id) missingFile = file
UI.postMessage({
event: 'generate-alert',
event: 'generate-alert', type: 'error',
message: `Unable to locate source file with any supported file extension: ${file}`,
file: missingFile
})
Expand Down Expand Up @@ -1272,7 +1272,7 @@ const getWavePredictBuffers = async ({
try {
wav.fromBuffer(chunk);
} catch (e) {
UI.postMessage({event: 'generate-alert', message: `Cannot parse ${file}, it has an invalid wav header.`});
UI.postMessage({event: 'generate-alert', type: 'error', message: `Cannot parse ${file}, it has an invalid wav header.`});
console.warn('GetWavePredictBuffers failed: ', e)
headerStream.close();
updateFilesBeingProcessed(file);
Expand Down Expand Up @@ -1533,7 +1533,7 @@ const fetchAudioBuffer = async ({
const stream = command.pipe();

command.on('error', error => {
UI.postMessage({event: 'generate-alert', message: error})
UI.postMessage({event: 'generate-alert', type: 'error', message: error})
reject(new Error('fetchAudioBuffer: Error extracting audio segment:', error));
});
command.on('start', function (commandLine) {
Expand Down Expand Up @@ -2562,7 +2562,7 @@ const getResults = async ({
filename += format == 'Raven' ? `_selections.txt` : '_detections.csv';
const filePath = p.join(directory, filename);
writeToPath(filePath, formattedValues, {headers: true, delimiter: format === 'Raven' ? '\t' : ','})
.on('error', err => UI.postMessage({event: 'generate-alert', message: `Cannot save file ${filePath}\nbecause it is open in another application`}))
.on('error', err => UI.postMessage({event: 'generate-alert', type: 'warning', message: `Cannot save file ${filePath}\nbecause it is open in another application`}))
.on('finish', () => {
UI.postMessage({event: 'generate-alert', message: filePath + ' has been written successfully.'});
});
Expand Down Expand Up @@ -2783,7 +2783,7 @@ const getSavedFileInfo = async (file) => {
}
return row
} else {
UI.postMessage({event: 'generate-alert', message: 'The database has not finished loading. The check for the presence of the file in the archive has been skipped'})
UI.postMessage({event: 'generate-alert', type: 'error', message: 'The database has not finished loading. The check for the presence of the file in the archive has been skipped'})
return undefined
}
};
Expand Down Expand Up @@ -3282,20 +3282,20 @@ async function onFileUpdated(oldName, newName){
});
} else {
UI.postMessage({
event: 'generate-alert', message: '<span class="text-danger">No changes made</span>. The selected file has a different duration to the original file.'
event: 'generate-alert', type: 'error', message: '<span class="text-danger">No changes made</span>. The selected file has a different duration to the original file.'
});
}
} catch (err) {
if (err.code === 'SQLITE_CONSTRAINT' && err.message.includes('UNIQUE')) {
// Unique constraint violation, show specific error message
UI.postMessage({
event: 'generate-alert',
event: 'generate-alert', type: 'warning',
message: '<span class="text-danger">No changes made</span>. The selected file already exists in the database.'
});
} else {
// Other types of errors
UI.postMessage({
event: 'generate-alert',
event: 'generate-alert', type: 'error',
message: `<span class="text-danger">An error occurred while updating the file: ${err.message}</span>`
});
}
Expand Down Expand Up @@ -3508,7 +3508,7 @@ async function setIncludedIDs(lat, lon, week) {

if (STATE.included === undefined) STATE.included = {}
STATE.included = merge(STATE.included, includedObject);
messages.forEach(message => UI.postMessage({event: 'generate-alert', message: message} ))
messages.forEach(message => UI.postMessage({event: 'generate-alert', type: 'warning', message: message} ))
return STATE.included;
})();

Expand Down Expand Up @@ -3689,7 +3689,7 @@ async function convertAndOrganiseFiles(threadLimit) {
if (!mkkDirFailed){
mkkDirFailed = true;
UI.postMessage({
event: 'generate-alert',
event: 'generate-alert', type: 'error',
message: `Failed to create directory: ${fullPath}<br>Error: ${err.message}`
});
}
Expand Down Expand Up @@ -3760,7 +3760,7 @@ async function convertFile(inputFilePath, fullFilePath, row, db, dbArchiveName,
let scaleFactor = 1;
if (STATE.detect.nocmig) {
if (boundaries.length > 1) {
UI.postMessage({event: 'generate-alert', message: `Multi-day operations are not yet supported: ${inputFilePath} will not be trimmed`});
UI.postMessage({event: 'generate-alert', type: 'warning', message: `Multi-day operations are not yet supported: ${inputFilePath} will not be trimmed`});
} else {
const {start, end} = boundaries[0];
if (start === end) return;
Expand Down Expand Up @@ -3788,7 +3788,7 @@ async function convertFile(inputFilePath, fullFilePath, row, db, dbArchiveName,
})
.on('error', (err) => {
DEBUG && console.error(`Error converting file ${inputFilePath}:`, err);
UI.postMessage({event: 'generate-alert', message: `File not found: ${inputFilePath}`, file: inputFilePath});
UI.postMessage({event: 'generate-alert', type: 'error', message: `File not found: ${inputFilePath}`, file: inputFilePath});
reject(err);
})
.on('progress', (progress) => {
Expand Down

0 comments on commit 521f6e3

Please sign in to comment.