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,