diff --git a/src/App.tsx b/src/App.tsx index 8abf98f..4c97748 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { + {import.meta.env.DEV && } ); } diff --git a/src/components/DemoPanel.tsx b/src/components/DemoPanel.tsx new file mode 100644 index 0000000..e87ba12 --- /dev/null +++ b/src/components/DemoPanel.tsx @@ -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[] = [ + // 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 = {}; + 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 ( +
+
+ Demo · Ctrl+Shift+D +
+

+ Inject a frozen worker-bar state, hide this panel, then screenshot. +

+
+ + + +
+
+ ); +}