fix: follow the POST-only autotag streams and the new LoRA and list contracts
autotag/download and autotag/start are POST-only now; EventSource can only
GET, so both pages stream their progress through SseStream.connect(url,
{method: 'POST'}), which keeps the onmessage/onerror/close surface the
handlers already use. autotag/single is called with image_id and the re-tag
dialog shows the server's filename instead of splitting an absolute path.
lora/status reports has_civitai_api_key instead of the key: the field shows a
'key is stored' placeholder, an empty field keeps the key on save and a
'Clear the stored key on save' checkbox sends clear_civitai_api_key.
Thumbnail generation reports per-file failures as file_error events; both
progress views count them (and show error_count from progress) without
closing the stream. The auto-tag review follows pagination.has_more on the
capped /images/all endpoint, and /search is paged like /recent so paging in
search mode no longer switches the list back to recent prompts.
This commit is contained in:
+50
-20
@@ -452,7 +452,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async search() {
|
||||
async search(page = 1) {
|
||||
const searchText = document.getElementById("searchText").value;
|
||||
const category = document.getElementById("searchCategory").value;
|
||||
const tags = document.getElementById("searchTags").value;
|
||||
@@ -467,21 +467,32 @@
|
||||
if (tags) params.append("tags", tags);
|
||||
const folder = document.getElementById("searchFolder").value;
|
||||
if (folder) params.append("folder", folder);
|
||||
params.append("limit", "100");
|
||||
const limit = this.pagination.limit;
|
||||
params.append("limit", String(limit));
|
||||
params.append("offset", String((page - 1) * limit));
|
||||
params.append("page", String(page));
|
||||
|
||||
this.listMode = "search";
|
||||
const url = `/prompt_manager/search?${PromptListSort.withSort(params, this.currentSort())}`;
|
||||
const data = await this.listRequest((signal) => this.api.get(url, { signal }));
|
||||
if (data === ApiClient.STALE) return; // a newer list request owns the loading state
|
||||
|
||||
const reported = data.pagination || {};
|
||||
const next = ListState.nextPageState(
|
||||
{ page: this.pagination.currentPage, limit, total: this.pagination.total },
|
||||
{ page: reported.page ?? page, total: reported.total ?? data.results.length },
|
||||
);
|
||||
this.prompts = data.results;
|
||||
this.pagination = { ...this.pagination, currentPage: next.page, total: next.total, totalPages: next.totalPages };
|
||||
this.renderPrompts();
|
||||
this.updatePaginationControls();
|
||||
document.getElementById("resultsTitle").textContent = "Search Results";
|
||||
} catch (error) {
|
||||
this.showNotification("Search failed", "error");
|
||||
console.error("Search error:", error);
|
||||
}
|
||||
this.showListLoaded();
|
||||
document.getElementById("paginationControls").classList.remove("hidden");
|
||||
}
|
||||
|
||||
currentSort() {
|
||||
@@ -495,7 +506,8 @@
|
||||
|
||||
/** Re-fetch the page currently shown, e.g. after a prompt was edited, tagged or deleted. */
|
||||
refreshList() {
|
||||
return this.listMode === "search" ? this.search() : this.loadRecentPrompts(this.pagination.currentPage);
|
||||
const page = this.pagination.currentPage;
|
||||
return this.listMode === "search" ? this.search(page) : this.loadRecentPrompts(page);
|
||||
}
|
||||
|
||||
handleActionClick(e) {
|
||||
@@ -828,7 +840,12 @@
|
||||
toggle.disabled = false;
|
||||
document.getElementById("loraManagerPath").value = status.path;
|
||||
document.getElementById("loraTriggerWords").checked = status.trigger_words_enabled;
|
||||
document.getElementById("civitaiApiKey").value = status.civitai_api_key || "";
|
||||
const keyInput = document.getElementById("civitaiApiKey");
|
||||
keyInput.value = "";
|
||||
keyInput.placeholder = status.has_civitai_api_key
|
||||
? "A key is stored — leave empty to keep it"
|
||||
: "Optional — required for NSFW example images";
|
||||
document.getElementById("clearCivitaiKey").checked = false;
|
||||
settings.classList.remove("hidden");
|
||||
this._loraPath = status.path;
|
||||
this._bindLoraEvents();
|
||||
@@ -884,18 +901,20 @@
|
||||
const enabled = document.getElementById("loraEnabled").checked;
|
||||
const triggerWords = document.getElementById("loraTriggerWords").checked;
|
||||
const civitaiKey = document.getElementById("civitaiApiKey").value.trim();
|
||||
const clearKey = document.getElementById("clearCivitaiKey").checked;
|
||||
const path = this._loraPath || "";
|
||||
|
||||
// The server never returns the stored key: an empty field keeps it, the
|
||||
// checkbox removes it, and a non-empty field replaces it
|
||||
const payload = { enabled, path, trigger_words_enabled: triggerWords };
|
||||
if (clearKey) payload.clear_civitai_api_key = true;
|
||||
else if (civitaiKey) payload.civitai_api_key = civitaiKey;
|
||||
|
||||
try {
|
||||
const res = await fetch("/prompt_manager/lora/enable", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
enabled,
|
||||
path,
|
||||
trigger_words_enabled: triggerWords,
|
||||
civitai_api_key: civitaiKey,
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) {
|
||||
@@ -2467,15 +2486,14 @@
|
||||
if (page < 1 || page > this.pagination.totalPages || page === this.pagination.currentPage) {
|
||||
return;
|
||||
}
|
||||
await this.loadRecentPrompts(page);
|
||||
await (this.listMode === "search" ? this.search(page) : this.loadRecentPrompts(page));
|
||||
// Scroll to top of results after page loads
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async changeLimit(newLimit) {
|
||||
this.pagination.limit = newLimit;
|
||||
this.pagination.currentPage = 1; // Reset to first page
|
||||
await this.loadRecentPrompts(1);
|
||||
await this.reloadPrompts(); // back to the first page of the current list
|
||||
}
|
||||
|
||||
// Metadata functionality
|
||||
@@ -3401,7 +3419,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
this.showModal("autoTagDownloadModal");
|
||||
|
||||
try {
|
||||
this.autoTagState.downloadEventSource = new EventSource(`/prompt_manager/autotag/download/${modelType}`);
|
||||
this.autoTagState.downloadEventSource = SseStream.connect(`/prompt_manager/autotag/download/${modelType}`, { method: 'POST' });
|
||||
|
||||
this.autoTagState.downloadEventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -3487,7 +3505,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
formData.append('prompt', prompt);
|
||||
}
|
||||
|
||||
this.autoTagState.eventSource = new EventSource(`/prompt_manager/autotag/start?${formData.toString()}`);
|
||||
this.autoTagState.eventSource = SseStream.connect(`/prompt_manager/autotag/start?${formData.toString()}`, { method: 'POST' });
|
||||
|
||||
this.autoTagState.eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -3561,10 +3579,9 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
// Get ALL images with linked prompts from the database
|
||||
try {
|
||||
const scanResponse = await fetch('/prompt_manager/images/all');
|
||||
const scanData = await scanResponse.json();
|
||||
const scanData = { images: await this.fetchAllImages() };
|
||||
|
||||
if (!scanData.success || !scanData.images || scanData.images.length === 0) {
|
||||
if (scanData.images.length === 0) {
|
||||
this.hideModal("autoTagLoadingModal");
|
||||
this.showNotification('No images with linked prompts found in database', 'warning');
|
||||
return;
|
||||
@@ -3611,7 +3628,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
// Set up the modal content
|
||||
document.getElementById('retagPreviewImage').src = imageUrl;
|
||||
document.getElementById('retagImageName').textContent = image.image_path.split('/').pop();
|
||||
document.getElementById('retagImageName').textContent = image.filename || String(image.image_path || '').split(/[\\/]/).pop();
|
||||
|
||||
// Display existing tags
|
||||
const tagsContainer = document.getElementById('retagExistingTags');
|
||||
@@ -3690,7 +3707,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
image_path: image.image_path,
|
||||
image_id: image.id,
|
||||
model_type: this.autoTagState.modelType,
|
||||
};
|
||||
if (this.autoTagState.modelType.startsWith('wd14')) {
|
||||
@@ -3724,6 +3741,19 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** /prompt_manager/images/all is capped per call; collect every page. */
|
||||
async fetchAllImages() {
|
||||
const images = [];
|
||||
let offset = 0;
|
||||
for (;;) {
|
||||
const data = await this.api.get(`/prompt_manager/images/all?offset=${offset}`);
|
||||
const page = Array.isArray(data.images) ? data.images : [];
|
||||
images.push(...page);
|
||||
if (!ListState.hasMorePages(data, page.length)) return images;
|
||||
offset += page.length;
|
||||
}
|
||||
}
|
||||
|
||||
renderReviewTags() {
|
||||
const visibleContainer = document.getElementById('reviewTagsVisible');
|
||||
const hiddenContainer = document.getElementById('reviewTagsHidden');
|
||||
|
||||
+31
-5
@@ -1951,7 +1951,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
// More detailed progress information
|
||||
const elapsedText = data.elapsed ? ` | Time: ${Math.floor(data.elapsed)}s` : '';
|
||||
progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped} | Rate: ${data.rate} img/s${elapsedText}`;
|
||||
const failedText = data.error_count ? `, Failed: ${data.error_count}` : '';
|
||||
progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped}${failedText} | Rate: ${data.rate} img/s${elapsedText}`;
|
||||
|
||||
// Update ETA
|
||||
if (data.eta > 0 && data.eta < 3600) {
|
||||
@@ -2003,6 +2004,17 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
resolve(resultData);
|
||||
});
|
||||
|
||||
// Per-file failures: count them, keep listening
|
||||
eventSource.addEventListener('file_error', (event) => {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.warn('Thumbnail failed:', data.current_file || data.file || '', data.error || '');
|
||||
} catch (e) {
|
||||
console.warn('Thumbnail failed (unparseable event)');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
if (cancelled) return;
|
||||
@@ -2676,7 +2688,8 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
// Update details
|
||||
const elapsedText = data.elapsed ? ` | Time: ${Math.floor(data.elapsed)}s` : '';
|
||||
progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped} | Rate: ${data.rate} img/s${elapsedText}`;
|
||||
const failedText = data.error_count ? `, Failed: ${data.error_count}` : '';
|
||||
progressDetails.textContent = `Generated: ${data.generated}, Skipped: ${data.skipped}${failedText} | Rate: ${data.rate} img/s${elapsedText}`;
|
||||
|
||||
// Update ETA
|
||||
if (data.eta > 0 && data.eta < 3600) {
|
||||
@@ -2731,6 +2744,19 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
resolve(resultData);
|
||||
});
|
||||
|
||||
// Per-file failures: report them, keep listening
|
||||
eventSource.addEventListener('file_error', (event) => {
|
||||
if (isCancelled()) return;
|
||||
let detail = 'Thumbnail failed';
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
detail = `Thumbnail failed: ${data.current_file || data.file || ''} ${data.error || ''}`.trim();
|
||||
} catch (e) {
|
||||
// non-JSON frame; keep the generic message
|
||||
}
|
||||
addStatusMessage(detail, 'error');
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
if (isCancelled()) {
|
||||
@@ -3183,7 +3209,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
this.showModal("autoTagDownloadModal");
|
||||
|
||||
try {
|
||||
this.autoTagState.downloadEventSource = new EventSource(`/prompt_manager/autotag/download/${modelType}`);
|
||||
this.autoTagState.downloadEventSource = SseStream.connect(`/prompt_manager/autotag/download/${modelType}`, { method: 'POST' });
|
||||
|
||||
this.autoTagState.downloadEventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -3295,7 +3321,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
formData.append('prompt', prompt);
|
||||
}
|
||||
|
||||
this.autoTagState.eventSource = new EventSource(`/prompt_manager/autotag/start?${formData.toString()}`);
|
||||
this.autoTagState.eventSource = SseStream.connect(`/prompt_manager/autotag/start?${formData.toString()}`, { method: 'POST' });
|
||||
|
||||
this.autoTagState.eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -3397,7 +3423,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
image_path: image.path,
|
||||
image_id: image.id,
|
||||
model_type: this.autoTagState.modelType,
|
||||
};
|
||||
if (this.autoTagState.modelType.startsWith('wd14')) {
|
||||
|
||||
Reference in New Issue
Block a user