Feat: Implement in-place UI updates after refresh

This commit is contained in:
Simon Cordt
2025-09-25 05:04:39 +02:00
parent b608e55889
commit ea3e2c869d
+183
View File
@@ -847,6 +847,189 @@
return card;
},
// In-place UI updates after refresh to avoid full re-render flicker
updateAfterRefresh(paged) {
try {
UI.updateToolbarControls();
UI.updateMetrics();
UI.updateRunningSection();
UI.updatePendingSection();
if (paged && paged.history) UI.mergeHistoryFromRefresh(paged);
UI.updateHistorySubtitle();
} catch (err) {
/* fallback: if anything fails, do a light rerender of sections only */
try { UI.ensureHistoryObserver(); } catch (e) { /* noop */ }
}
},
updateToolbarControls() {
try {
const pauseBtn = document.getElementById('pqueue-toggle');
if (pauseBtn) {
const isPaused = !!state.paused;
pauseBtn.title = isPaused ? 'Resume queue' : 'Pause queue';
pauseBtn.setAttribute('aria-label', isPaused ? 'Resume queue' : 'Pause queue');
pauseBtn.classList.toggle('pqueue-button--success', isPaused);
pauseBtn.classList.toggle('pqueue-button--warning', !isPaused);
pauseBtn.innerHTML = '';
pauseBtn.appendChild(UI.icon(isPaused ? 'ti ti-player-play-filled' : 'ti ti-player-pause-filled'));
// Ensure handler remains attached
pauseBtn.removeEventListener('click', Events.togglePause);
pauseBtn.addEventListener('click', Events.togglePause);
}
} catch (err) { /* noop */ }
},
updateMetrics() {
try {
const newMetrics = UI.renderMetrics();
if (!newMetrics) return;
const old = state.dom.metrics;
if (old && old.parentNode) {
old.parentNode.replaceChild(newMetrics, old);
state.dom.metrics = newMetrics;
}
} catch (err) { /* noop */ }
},
updateRunningSection() {
try {
UI.withStableAnchor(() => {
const card = state.dom.runningCard;
if (!card) return;
const body = card.querySelector('.pqueue-card__body');
if (!body) return;
body.innerHTML = '';
if (!state.queue_running.length) {
body.appendChild(UI.emptyState({ icon: 'ti ti-player-play', title: 'No running job', description: 'Prompt will start running after the queue resumes' }));
} else {
state.queue_running.forEach((item, index) => {
const pid = item[1];
const fraction = Math.max(0, Math.min(1, Number(state.running_progress?.[pid]) || 0));
const workflowName = state.workflowNameCache.get(pid) || pid;
const meta = UI.el('div', { class: 'pqueue-item__meta' }, [
UI.icon('ti ti-loader-2', { spin: true }),
UI.el('span', { class: 'pqueue-chip pqueue-chip--primary', text: `#${index + 1}` }),
UI.el('span', { class: 'pqueue-row__label', text: workflowName }),
UI.el('span', { class: 'pqueue-progress__label', text: Format.percent(fraction) }),
]);
const bar = UI.el('div', { class: 'pqueue-progress-bar' });
bar.style.width = `${(fraction * 100).toFixed(1)}%`;
const progress = UI.el('div', { class: 'pqueue-progress' }, [UI.el('div', { class: 'pqueue-progress__track' }, [bar])]);
body.appendChild(UI.el('div', { class: 'pqueue-item pqueue-item--running', 'data-id': pid }, [meta, progress]));
});
}
const subtitle = card.querySelector('.pqueue-card__subtitle');
if (subtitle) subtitle.textContent = state.metrics.runningCount ? `${state.metrics.runningCount} active` : '';
});
} catch (err) { /* noop */ }
},
updatePendingSection() {
try {
UI.withStableAnchor(() => {
const card = state.dom.pendingCard;
if (!card) return;
const subtitle = card.querySelector('.pqueue-card__subtitle');
if (subtitle) subtitle.textContent = state.metrics.queueCount ? `${state.metrics.queueCount} pending` : '';
const table = state.dom.pendingTable;
if (!table) return;
// Remove existing rows
Array.from(table.querySelectorAll('.pqueue-row[data-id]')).forEach((n) => n.remove());
// Rebuild rows
const rows = state.queue_pending.map(UI.pendingRow).filter(Boolean);
const header = table.querySelector('.pqueue-list__header');
const frag = document.createDocumentFragment();
rows.forEach((r) => frag.appendChild(r));
if (header && header.nextSibling) {
table.insertBefore(frag, header.nextSibling);
} else {
table.appendChild(frag);
}
// Update footer and selection visuals
UI.refreshFilter();
UI.updateSelectionUI();
UI.updatePendingFooter(Array.from(table.querySelectorAll('.pqueue-row[data-id]')).filter((r) => r.style.display !== 'none').length);
});
} catch (err) { /* noop */ }
},
mergeHistoryFromRefresh(paged) {
try {
const grid = state.dom.historyGrid;
const sentinel = state.dom.historySentinel;
if (!grid || !sentinel) return;
// Anchor-based scroll preservation using the real sidebar scroller
const scroller = UI.getScrollContainer();
const firstExisting = grid.querySelector('.pqueue-history-card');
let anchorTopBefore = 0;
if (scroller && firstExisting) {
const scRect = scroller.getBoundingClientRect();
const aRect = firstExisting.getBoundingClientRect();
anchorTopBefore = aRect.top - scRect.top;
}
let list = Array.isArray(paged?.history) ? paged.history : [];
// Ensure list order matches current sort_dir so first card is newest in 'desc'
try {
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
list = list.slice().sort((a, b) => {
const ak = Date.parse(a?.completed_at || a?.created_at || 0) || (a?.id || 0);
const bk = Date.parse(b?.completed_at || b?.created_at || 0) || (b?.id || 0);
return dir === 'desc' ? (bk - ak) : (ak - bk);
});
} catch (err) { /* noop */ }
if (!state.historyIds) state.historyIds = new Set();
let added = 0;
for (const row of list) {
const id = row?.id;
if (id == null || state.historyIds.has(id)) continue;
state.historyIds.add(id);
const card = UI.historyCard(row);
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
// Determine insertion point by timestamp or id, matching current sort
const key = Number(card.getAttribute('data-key') || 0);
const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card'));
let inserted = false;
for (const existing of existingCards) {
const otherKey = Number(existing.getAttribute('data-key') || 0);
if (dir === 'desc') {
if (key >= otherKey) { grid.insertBefore(card, existing); inserted = true; break; }
} else {
if (key <= otherKey) { grid.insertBefore(card, existing); inserted = true; break; }
}
}
if (!inserted) grid.insertBefore(card, sentinel);
// Maintain in-memory list order similarly
if (Array.isArray(state.history)) {
if (dir === 'desc') state.history.unshift(row);
else state.history.push(row);
}
added += 1;
}
if (added) {
// Restore scroll to keep the previous first card at the same offset
if (scroller && firstExisting) {
requestAnimationFrame(() => {
try {
const scRect2 = scroller.getBoundingClientRect();
const aRect2 = firstExisting.getBoundingClientRect();
const newOffset = aRect2.top - scRect2.top;
const delta = newOffset - anchorTopBefore;
scroller.scrollTop = Math.max(0, scroller.scrollTop + delta);
} catch (err) { /* noop */ }
});
}
}
if (typeof paged?.total === 'number') state.historyTotal = paged.total;
state.historyPaging = state.historyPaging || { isLoading: false, hasMore: true, nextCursor: null, params: { sort_by: 'id', sort_dir: 'desc', limit: 60 } };
state.historyPaging.nextCursor = paged?.next_cursor || state.historyPaging.nextCursor;
state.historyPaging.hasMore = !!paged?.has_more || state.historyPaging.hasMore;
if (!state.historyPaging.hasMore && sentinel) sentinel.style.display = 'none';
} catch (err) { /* noop */ }
},
currentHistoryRangeLabel() {
try {
const sDate = state.filters?.historySince || "";