diff --git a/tests/js/workspace-activity-kinds.test.js b/tests/js/workspace-activity-kinds.test.js index 84831a6..68d3afc 100644 --- a/tests/js/workspace-activity-kinds.test.js +++ b/tests/js/workspace-activity-kinds.test.js @@ -225,3 +225,26 @@ test('a failed duplicate cleanup says the server reason (S7 review M3)', async ( assert.equal(finishToast(failed), `Duplicate cleanup failed · ${reason}`); assert.equal(finishToast({ ...failed, status_line: '' }), 'Duplicate cleanup failed · 0 removed · 1 error'); }); + +// #328: Tag output images (`autotag`) reuses the image tagging view: Tagged / Unchanged / Skipped / Errors and Cancel. +// The legacy page's hold of the same kind counts nothing and is not cancellable, so it keeps the generic held view. +test('autotag gets Tagged / Unchanged / Skipped / Errors and Cancel; its legacy hold keeps the generic view', async () => { + const { kindOf, viewOf, canCancel } = await source; + const { startToast, finishToast } = await store; + const counters = { tagged: 3, unchanged: 1, skipped: 2, errors: 0 }; + const batch = job({ kind: 'autotag', title: 'Tag output images', counters, scope: { images: 6 } }); + const view = viewOf(batch); + assert.deepEqual(view.tiles.map(([group, label]) => [group, label]), + [['tagged', 'Tagged'], ['unchanged', 'Unchanged'], ['skipped', 'Skipped'], ['errors', 'Errors']]); + assert.equal(kindOf('autotag').pillVerb, 'Tagging'); + assert.equal(kindOf('autotag').cancelLabel, 'Cancel tagging'); + assert.equal(canCancel(batch), true); + assert.equal(startToast(batch), 'Tag output images started.'); + assert.match(finishToast({ ...batch, state: 'finished', finished_at: 2 }), /^Tag output images finished/); + + const held = job({ kind: 'autotag', title: 'Tag output images (legacy page)', counters: {}, cancellable: false, + progress: 0, processed: 0, total: 0 }); + const legacy = viewOf(held); + assert.deepEqual([legacy.tiles, legacy.outcomes, [...legacy.openable]], [[], [], []]); + assert.equal(canCancel(held), false); +}); diff --git a/web/js/admin.js b/web/js/admin.js index b02fb90..4f1e6ae 100644 --- a/web/js/admin.js +++ b/web/js/admin.js @@ -3966,6 +3966,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; } document.getElementById('reviewImage').src = imageUrl; + // The record's file name, else the last segment of its path (#328: `filename` was never declared). + const filename = image.filename || String(image.image_path || image.path || '').split(/[\\/]/).pop(); document.getElementById('reviewImageName').textContent = filename || 'Unknown'; document.getElementById('reviewTagsVisible').innerHTML = '
Generating tags...
'; document.getElementById('reviewTagsHidden').innerHTML = ''; diff --git a/web/js/gallery.js b/web/js/gallery.js index 97834b0..95d010e 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -3543,29 +3543,25 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`; this.autoTagState.eventSource.onmessage = (event) => { const data = JSON.parse(event.data); - if (data.status === 'loading_model') { - document.getElementById('autoTagCurrentFile').textContent = 'Loading model...'; - } else if (data.status === 'processing') { - const percent = Math.round((data.processed / data.total) * 100); - document.getElementById('autoTagCurrentFile').textContent = data.current_file || 'Processing...'; - document.getElementById('autoTagProgressPercent').textContent = `${percent}%`; - document.getElementById('autoTagProgressBar').style.width = `${percent}%`; - document.getElementById('autoTagProcessed').textContent = data.processed; - document.getElementById('autoTagApplied').textContent = data.tags_applied; - document.getElementById('autoTagSkipped').textContent = data.skipped; - } else if (data.status === 'complete') { + // The server sends `type` frames, as the admin page reads them (#328). + if (data.type === 'progress') { + document.getElementById('autoTagCurrentFile').textContent = data.status || 'Processing...'; + document.getElementById('autoTagProgressPercent').textContent = `${data.progress || 0}%`; + document.getElementById('autoTagProgressBar').style.width = `${data.progress || 0}%`; + if (data.processed !== undefined) { + document.getElementById('autoTagProcessed').textContent = data.processed; + document.getElementById('autoTagApplied').textContent = data.tagged || 0; + document.getElementById('autoTagSkipped').textContent = data.skipped || 0; + } + } else if (data.type === 'complete') { this.autoTagState.eventSource.close(); this.hideModal("autoTagProgressModal"); - this.showNotification(`Auto tagging complete! Applied tags to ${data.tags_applied} prompts.`, 'success'); + this.showNotification(`Auto tagging complete! Applied tags to ${data.tagged || 0} prompts.`, 'success'); this.loadImages(); // Refresh the gallery - } else if (data.status === 'error') { + } else if (data.type === 'error') { this.autoTagState.eventSource.close(); this.hideModal("autoTagProgressModal"); this.showNotification(`Auto tag error: ${data.message}`, 'error'); - } else if (data.status === 'cancelled') { - this.autoTagState.eventSource.close(); - this.hideModal("autoTagProgressModal"); - this.showNotification('Auto tagging cancelled', 'info'); } }; diff --git a/web/js/workspace/activity-kinds.mjs b/web/js/workspace/activity-kinds.mjs index 4bed30c..8e7d079 100644 --- a/web/js/workspace/activity-kinds.mjs +++ b/web/js/workspace/activity-kinds.mjs @@ -104,6 +104,24 @@ function maintenanceFinish(job) { // D85: an image tagging job says which of its two modes started. const IMAGE_TAG_START_TOASTS = Object.freeze({ add: 'Tagging started.', auto: 'Auto-tagging started.' }); +/** D89: the image tagging job's view, shared with Tag output images (#328). */ +const IMAGE_TAGS = Object.freeze({ + ...GENERIC, + pillVerb: 'Tagging', + startToast: job => IMAGE_TAG_START_TOASTS[job.scope?.mode] ?? titleStarted(job), + finishToast: namedFinish(job => job.title, taggedOf, tagLine), + line: tagLine, + kept: taggedOf, + scope: imagesOf, + tiles: tiles([['tagged', 'Tagged', 'added'], ['unchanged', 'Unchanged', ''], ['skipped', 'Skipped', 'warn'], + ['errors', 'Errors', 'bad']]), + outcomes: tiles([['tagged', 'Tagged'], ['errors', 'Errors'], ['skipped', 'Skipped'], ['unchanged', 'Unchanged']]), + openable: groups('tagged', 'unchanged'), + log: 'Tagging log', + cancelLabel: 'Cancel tagging', + cancel: 'Cancel tagging', +}); + /** Every kind with a descriptor of its own. */ export const KINDS = Object.freeze({ __proto__: null, @@ -125,22 +143,10 @@ export const KINDS = Object.freeze({ cancel: 'Cancel scan', }), // D89: Tagged (green), Unchanged, Skipped (warning) and Errors (red); Tagged and Unchanged rows open the viewer. - 'image-tags': Object.freeze({ - ...GENERIC, - pillVerb: 'Tagging', - startToast: job => IMAGE_TAG_START_TOASTS[job.scope?.mode] ?? titleStarted(job), - finishToast: namedFinish(job => job.title, taggedOf, tagLine), - line: tagLine, - kept: taggedOf, - scope: imagesOf, - tiles: tiles([['tagged', 'Tagged', 'added'], ['unchanged', 'Unchanged', ''], ['skipped', 'Skipped', 'warn'], - ['errors', 'Errors', 'bad']]), - outcomes: tiles([['tagged', 'Tagged'], ['errors', 'Errors'], ['skipped', 'Skipped'], ['unchanged', 'Unchanged']]), - openable: groups('tagged', 'unchanged'), - log: 'Tagging log', - cancelLabel: 'Cancel tagging', - cancel: 'Cancel tagging', - }), + 'image-tags': IMAGE_TAGS, + // #328 (extends D111): Tag output images, the prompt-tag batch, counts its images the same way. Its legacy-page + // hold counts nothing and is not cancellable, so `viewOf` gives it the generic held view. + autotag: IMAGE_TAGS, // D111: Generated (green), Skipped (already fresh) and Errors (red); a row with a preview opens the viewer. thumbnails: Object.freeze({ ...GENERIC,