chore(dev): add a dev-only demo panel for screenshotting transient UI
The background-worker pipeline drains in under a second on a fast machine, making it impossible to screenshot live. This adds a dev-only panel (Ctrl+Shift+D) that injects a frozen mid-pipeline state into mediaJobProgress so the worker bar can be captured at leisure, then cleared. Gated by import.meta.env.DEV and verified absent from the production bundle, so it never ships. A first rough cut of the broader screenshot/demo mode.
This commit is contained in:
@@ -12,6 +12,7 @@ import { TitleBar } from "./components/TitleBar";
|
||||
import { SettingsModal } from "./components/SettingsModal";
|
||||
import { UpdateToast } from "./components/UpdateToast";
|
||||
import { OnboardingOverlay } from "./components/onboarding/OnboardingOverlay";
|
||||
import { DemoPanel } from "./components/DemoPanel";
|
||||
import { initializeNotifications } from "./notifications";
|
||||
|
||||
export default function App() {
|
||||
@@ -94,6 +95,7 @@ export default function App() {
|
||||
<SettingsModal />
|
||||
<UpdateToast />
|
||||
<OnboardingOverlay />
|
||||
{import.meta.env.DEV && <DemoPanel />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FolderJobProgress, useGalleryStore } from "../store";
|
||||
|
||||
// Dev-only screenshot/demo helper. Injects frozen UI states that are otherwise
|
||||
// too transient to capture (e.g. the background-worker pipeline, which drains
|
||||
// in under a second on a fast machine). Gated by import.meta.env.DEV in App.tsx
|
||||
// so the whole thing is tree-shaken out of production builds.
|
||||
//
|
||||
// Toggle the controls with Ctrl+Shift+D. Injected state persists in the store
|
||||
// while the panel is hidden, so: open → inject → hide → screenshot.
|
||||
|
||||
function emptyProgress(folderId: number): FolderJobProgress {
|
||||
return {
|
||||
folder_id: folderId,
|
||||
thumbnail_pending: 0,
|
||||
metadata_pending: 0,
|
||||
embedding_pending: 0,
|
||||
embedding_ready: 0,
|
||||
embedding_failed: 0,
|
||||
caption_pending: 0,
|
||||
caption_ready: 0,
|
||||
caption_failed: 0,
|
||||
tagging_pending: 0,
|
||||
tagging_ready: 0,
|
||||
tagging_failed: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// A believable multi-folder "busy pipeline" — three folders at different stages.
|
||||
const BUSY_PRESETS: Partial<FolderJobProgress>[] = [
|
||||
// Embeddings actively running, tagging queued behind it.
|
||||
{ embedding_ready: 34, embedding_pending: 16, tagging_pending: 50 },
|
||||
// Just started: thumbnails draining, everything else queued.
|
||||
{ thumbnail_pending: 11, embedding_pending: 50, tagging_pending: 50 },
|
||||
// Late stage: embeddings done, tagging running.
|
||||
{ embedding_ready: 40, tagging_ready: 18, tagging_pending: 22 },
|
||||
];
|
||||
|
||||
export function DemoPanel() {
|
||||
const folders = useGalleryStore((state) => state.folders);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === "d") {
|
||||
event.preventDefault();
|
||||
setOpen((value) => !value);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
const injectBusy = () => {
|
||||
const progress: Record<number, FolderJobProgress> = {};
|
||||
folders.slice(0, BUSY_PRESETS.length).forEach((folder, index) => {
|
||||
progress[folder.id] = { ...emptyProgress(folder.id), ...BUSY_PRESETS[index] };
|
||||
});
|
||||
useGalleryStore.setState({ mediaJobProgress: progress });
|
||||
};
|
||||
|
||||
const injectSingleEmbedding = () => {
|
||||
const folder = folders[0];
|
||||
if (!folder) return;
|
||||
useGalleryStore.setState({
|
||||
mediaJobProgress: {
|
||||
[folder.id]: { ...emptyProgress(folder.id), embedding_ready: 27, embedding_pending: 23 },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clear = () => useGalleryStore.setState({ mediaJobProgress: {} });
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 z-[100] w-56 rounded-lg border border-amber-400/40 bg-amber-950/90 p-3 text-xs text-amber-100 shadow-xl backdrop-blur">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-semibold uppercase tracking-wide">Demo · Ctrl+Shift+D</span>
|
||||
</div>
|
||||
<p className="mb-2 text-[11px] leading-snug text-amber-200/70">
|
||||
Inject a frozen worker-bar state, hide this panel, then screenshot.
|
||||
</p>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<button
|
||||
className="rounded-md border border-amber-400/30 bg-amber-500/15 px-2 py-1.5 text-left hover:bg-amber-500/25"
|
||||
onClick={injectBusy}
|
||||
>
|
||||
Pipeline: busy (3 folders)
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md border border-amber-400/30 bg-amber-500/15 px-2 py-1.5 text-left hover:bg-amber-500/25"
|
||||
onClick={injectSingleEmbedding}
|
||||
>
|
||||
Pipeline: embedding (1 folder)
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md border border-white/10 bg-white/[0.06] px-2 py-1.5 text-left text-gray-300 hover:bg-white/10"
|
||||
onClick={clear}
|
||||
>
|
||||
Clear injected state
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user