Spaces:
Sleeping
Sleeping
File size: 4,390 Bytes
37ee895 89b6a21 7d5735a 89b6a21 127bfda 89b6a21 7d5735a 89b6a21 127bfda 89b6a21 127bfda 89b6a21 127bfda 89b6a21 127bfda 89b6a21 7d5735a 89b6a21 127bfda 89b6a21 127bfda 89b6a21 37ee895 89b6a21 7d5735a 89b6a21 37ee895 7d5735a 37ee895 7d5735a 37ee895 7d5735a 37ee895 7d5735a 37ee895 7d5735a 37ee895 89b6a21 32f24ec 89b6a21 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | import { fetchClipJobs, fetchVisionStatus, postVisionIngest } from "./api.js";
import { renderClipCard, renderVisionStatus } from "./view.js";
// Adaptive polling based on Page Visibility API.
class AdaptivePoller {
constructor(fn, intervalMs = 2000) {
this.fn = fn;
this.baseInterval = intervalMs;
this.timer = null;
this.isRunning = false;
document.addEventListener("visibilitychange", () => {
if (this.isRunning) {
this.restart(); // Restart with interval adjusted by visibility
}
});
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.tick();
}
stop() {
this.isRunning = false;
if (this.timer) clearTimeout(this.timer);
}
restart() {
if (this.timer) clearTimeout(this.timer);
this.tick();
}
async tick() {
if (!this.isRunning) return;
await this.fn();
const isHidden = document.hidden;
const nextInterval = isHidden ? this.baseInterval * 5 : this.baseInterval;
this.timer = setTimeout(() => this.tick(), nextInterval);
}
}
export function createClipsController({ els }) {
const render = (jobs) => {
if (!els.container) return;
if (jobs.length === 0) {
els.container.innerHTML = '<div class="empty-state">No recent clip jobs.</div>';
return;
}
const html = jobs.map(renderClipCard).join("");
// Simple DOM diffing could be added here for optimization,
// but innerHTML is fast enough for < 50 items.
// We check if content changed to avoid text selection loss if user is selecting text.
if (els.container.innerHTML !== html) {
els.container.innerHTML = html;
}
// Hide skeleton if visible
if (els.loadingSkeleton) {
els.loadingSkeleton.style.display = "none";
}
};
const fetchAndRender = async () => {
try {
const [clipData, visionData] = await Promise.all([
fetchClipJobs().catch(() => null),
fetchVisionStatus().catch(() => null)
]);
if (clipData?.ok && Array.isArray(clipData.items)) {
render(clipData.items);
}
if (visionData?.ok) {
renderVisionStatus(visionData, els);
}
} catch (error) {
console.error("Clip polling error:", error);
}
};
const handleVisionIngest = async () => {
if (!els.visionIngestInput || !els.visionIngestInput.files.length) {
if (els.visionFeedback) els.visionFeedback.textContent = "Select an image first.";
return;
}
const file = els.visionIngestInput.files[0];
if (els.visionIngestBtn) els.visionIngestBtn.disabled = true;
if (els.visionFeedback) els.visionFeedback.textContent = "Uploading frame...";
try {
const result = await postVisionIngest(file);
if (result && result.ok) {
els.visionFeedback.textContent = "Frame analyzed successfully.";
els.visionFeedback.className = "panel-hint event-level-info";
els.visionIngestInput.value = ""; // clear
await fetchAndRender();
} else {
els.visionFeedback.textContent = `Error: ${result?.reason || 'Upload failed'}`;
els.visionFeedback.className = "panel-hint event-level-warn";
}
} catch (error) {
els.visionFeedback.textContent = "Error communicating with server.";
els.visionFeedback.className = "panel-hint event-level-error";
} finally {
if (els.visionIngestBtn) els.visionIngestBtn.disabled = false;
}
};
if (els.visionIngestBtn) {
els.visionIngestBtn.addEventListener("click", handleVisionIngest);
}
const poller = new AdaptivePoller(fetchAndRender, 2000);
return {
startPolling: () => poller.start(),
stopPolling: () => poller.stop(),
manualRefresh: fetchAndRender,
// O6/reliability: render from the SSE snapshot instead of polling.
renderFromSnapshot: (clips) => {
if (clips && Array.isArray(clips.items)) render(clips.items);
}
};
}
|