Spaces:
Paused
Paused
Shereen Lee commited on
Commit ·
3ff4ec5
1
Parent(s): 7ab786f
fix: remove raw-id badge from scrapbook moment cards; lock photo browser + add Cancel while a caption/title is generating
Browse files- index.html +59 -8
index.html
CHANGED
|
@@ -1707,7 +1707,13 @@
|
|
| 1707 |
// ----------------------------------------------------------------- //
|
| 1708 |
// components/details-form.tsx
|
| 1709 |
// ----------------------------------------------------------------- //
|
| 1710 |
-
function DetailsForm({ title, onTitleChange, caption, onCaptionChange, date, onDateChange, time, onTimeChange, tags, onTagsChange, onSubmit, onAutoCaption, autoBusy, autoStage, autoError, captionPrompt, onCaptionPromptChange, captionModel, onAutoTitle, titleBusy, titleStage, titleError, titlePrompt, onTitlePromptChange, onAutoTags, tagsBusy, tagsStage, tagsError, tagsPrompt, onTagsPromptChange }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1711 |
const [showPrompt, setShowPrompt] = useState(false);
|
| 1712 |
const [showTitlePrompt, setShowTitlePrompt] = useState(false);
|
| 1713 |
const [showTagsPrompt, setShowTagsPrompt] = useState(false);
|
|
@@ -1782,6 +1788,7 @@
|
|
| 1782 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "150ms" }} />
|
| 1783 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "300ms" }} />
|
| 1784 |
</span>
|
|
|
|
| 1785 |
</div>
|
| 1786 |
) : null}
|
| 1787 |
</div>
|
|
@@ -1849,6 +1856,7 @@
|
|
| 1849 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "150ms" }} />
|
| 1850 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "300ms" }} />
|
| 1851 |
</span>
|
|
|
|
| 1852 |
</div>
|
| 1853 |
) : null}
|
| 1854 |
</div>
|
|
@@ -2155,7 +2163,23 @@
|
|
| 2155 |
removingRef.current = false; // allow the next click immediately
|
| 2156 |
}
|
| 2157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2158 |
function changePhoto(delta) {
|
|
|
|
| 2159 |
if (stackPhotos.length <= 1) return; // nothing to browse
|
| 2160 |
setSlideDir(delta > 0 ? "right" : "left");
|
| 2161 |
setPhotoIndex((prev) => {
|
|
@@ -2190,17 +2214,26 @@
|
|
| 2190 |
if (!photo) return;
|
| 2191 |
if (window.sozaiPing) window.sozaiPing("✨ auto-generating caption");
|
| 2192 |
setAutoBusy(true); setAutoError(null);
|
|
|
|
|
|
|
| 2193 |
let stageIdx = 0;
|
| 2194 |
setAutoStage(AUTO_STAGES[0]);
|
| 2195 |
const stageTimer = setInterval(() => {
|
| 2196 |
stageIdx = Math.min(stageIdx + 1, AUTO_STAGES.length - 1);
|
| 2197 |
setAutoStage(AUTO_STAGES[stageIdx]);
|
| 2198 |
}, 900);
|
|
|
|
| 2199 |
const started = Date.now();
|
| 2200 |
const finish = (fn) => {
|
| 2201 |
// ensure the lovely animation is visible for at least a beat
|
| 2202 |
const wait = Math.max(0, 700 - (Date.now() - started));
|
| 2203 |
-
setTimeout(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2204 |
};
|
| 2205 |
try {
|
| 2206 |
const gr = await window.__gradioReady;
|
|
@@ -2226,16 +2259,25 @@
|
|
| 2226 |
if (!photo) return;
|
| 2227 |
if (window.sozaiPing) window.sozaiPing("✨ auto-generating title");
|
| 2228 |
setTitleBusy(true); setTitleError(null);
|
|
|
|
|
|
|
| 2229 |
let stageIdx = 0;
|
| 2230 |
setTitleStage(TITLE_STAGES[0]);
|
| 2231 |
const stageTimer = setInterval(() => {
|
| 2232 |
stageIdx = Math.min(stageIdx + 1, TITLE_STAGES.length - 1);
|
| 2233 |
setTitleStage(TITLE_STAGES[stageIdx]);
|
| 2234 |
}, 900);
|
|
|
|
| 2235 |
const started = Date.now();
|
| 2236 |
const finish = (fn) => {
|
| 2237 |
const wait = Math.max(0, 700 - (Date.now() - started));
|
| 2238 |
-
setTimeout(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2239 |
};
|
| 2240 |
// a title-specific default so a blank prompt still yields a *title*,
|
| 2241 |
// kept independent from the caption prompt.
|
|
@@ -2288,16 +2330,25 @@
|
|
| 2288 |
if (!photo) return;
|
| 2289 |
if (window.sozaiPing) window.sozaiPing("🏷️ auto-generating tags");
|
| 2290 |
setTagsBusy(true); setTagsError(null);
|
|
|
|
|
|
|
| 2291 |
let stageIdx = 0;
|
| 2292 |
setTagsStage(TAGS_STAGES[0]);
|
| 2293 |
const stageTimer = setInterval(() => {
|
| 2294 |
stageIdx = Math.min(stageIdx + 1, TAGS_STAGES.length - 1);
|
| 2295 |
setTagsStage(TAGS_STAGES[stageIdx]);
|
| 2296 |
}, 900);
|
|
|
|
| 2297 |
const started = Date.now();
|
| 2298 |
const finish = (fn) => {
|
| 2299 |
const wait = Math.max(0, 700 - (Date.now() - started));
|
| 2300 |
-
setTimeout(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2301 |
};
|
| 2302 |
try {
|
| 2303 |
const gr = await window.__gradioReady;
|
|
@@ -2403,13 +2454,13 @@
|
|
| 2403 |
<input ref={addPhotoRef} type="file" accept={ACCEPT_ATTR} multiple className="hidden" onChange={onAddPhotos} />
|
| 2404 |
{stackPhotos.length >= 1 && (
|
| 2405 |
<div className="flex items-start gap-3">
|
| 2406 |
-
<button type="button" onClick={() => changePhoto(-1)} aria-label="Previous photo" className="sozai-sway-l flex size-9 items-center justify-center rounded-full border-2 border-primary/25 text-primary/55 transition-all hover:border-primary hover:bg-accent hover:text-primary"><ChevronLeft className="size-4" /></button>
|
| 2407 |
<div className="flex flex-col items-center gap-1">
|
| 2408 |
<button type="button" onClick={() => addPhotoRef.current && addPhotoRef.current.click()} aria-label="Add photos" title="Add photos" className="flex size-9 items-center justify-center rounded-full border-2 border-primary bg-primary text-primary-foreground transition-colors hover:opacity-90"><Plus className="size-4" /></button>
|
| 2409 |
<span className="font-mono text-[9px] uppercase tracking-wide text-primary">upload</span>
|
| 2410 |
</div>
|
| 2411 |
<button type="button" onClick={onRemovePhoto} aria-label="Remove this photo" title="Remove this photo" className="flex size-9 items-center justify-center rounded-full border-2 border-[#800020] bg-[#800020] text-white transition-colors hover:opacity-90"><Trash2 className="size-4" /></button>
|
| 2412 |
-
<button type="button" onClick={() => changePhoto(1)} aria-label="Next photo" className="sozai-sway-r flex size-9 items-center justify-center rounded-full border-2 border-primary/25 text-primary/55 transition-all hover:border-primary hover:bg-accent hover:text-primary"><ChevronRight className="size-4" /></button>
|
| 2413 |
</div>
|
| 2414 |
)}
|
| 2415 |
{/* How this works - fills the left-column negative space */}
|
|
@@ -2436,7 +2487,8 @@
|
|
| 2436 |
onAutoTitle={handleAutoTitle} titleBusy={titleBusy} titleStage={titleStage} titleError={titleError}
|
| 2437 |
titlePrompt={settings.titlePrompt || ""} onTitlePromptChange={setTitlePrompt}
|
| 2438 |
onAutoTags={handleAutoTags} tagsBusy={tagsBusy} tagsStage={tagsStage} tagsError={tagsError}
|
| 2439 |
-
tagsPrompt={settings.tagsPrompt || ""} onTagsPromptChange={(v) => update({ tagsPrompt: v })}
|
|
|
|
| 2440 |
{validationError ? <p role="alert" className="rounded-md border-2 border-[oklch(0.55_0.18_25)]/50 bg-[oklch(0.55_0.18_25)]/10 px-3 py-2 text-center font-mono text-[11px] uppercase tracking-wide text-[oklch(0.5_0.18_25)]">{validationError}</p> : null}
|
| 2441 |
{confirmation ? <p role="status" className="rounded-md border-2 border-primary/40 bg-accent px-3 py-2 text-center text-sm text-accent-foreground">{confirmation}</p> : null}
|
| 2442 |
|
|
@@ -2493,7 +2545,6 @@
|
|
| 2493 |
<button type="button" onClick={() => onSelect && onSelect(moment.id)} style={{ left: `${moment.x}%`, top: `${moment.y}%` }}
|
| 2494 |
className={cn("group absolute z-10 w-44 -translate-x-1/2 -translate-y-1/2 rounded-sm border-2 border-primary bg-card p-2 text-left shadow-md transition-transform hover:z-20 hover:scale-105 sm:w-52", rotate, selected && "z-20 ring-4 ring-primary/30")}>
|
| 2495 |
<span aria-hidden="true" className="absolute -top-2.5 left-1/2 h-4 w-12 -translate-x-1/2 -rotate-2 rounded-sm bg-primary/15" />
|
| 2496 |
-
<span className="absolute right-2 top-2 z-10 flex size-6 items-center justify-center rounded-full border-2 border-primary bg-card font-mono text-xs font-bold text-primary">{moment.id}</span>
|
| 2497 |
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-sm border border-primary/30"><Img src={moment.src} alt={moment.alt} fill sizes="208px" className="object-cover [filter:saturate(0.85)_contrast(1.05)]" /></div>
|
| 2498 |
<div className="px-0.5 pb-0.5 pt-2">
|
| 2499 |
<div className="flex items-start justify-between gap-1">
|
|
|
|
| 1707 |
// ----------------------------------------------------------------- //
|
| 1708 |
// components/details-form.tsx
|
| 1709 |
// ----------------------------------------------------------------- //
|
| 1710 |
+
function DetailsForm({ title, onTitleChange, caption, onCaptionChange, date, onDateChange, time, onTimeChange, tags, onTagsChange, onSubmit, onAutoCaption, autoBusy, autoStage, autoError, captionPrompt, onCaptionPromptChange, captionModel, onAutoTitle, titleBusy, titleStage, titleError, titlePrompt, onTitlePromptChange, onAutoTags, tagsBusy, tagsStage, tagsError, tagsPrompt, onTagsPromptChange, onCancelGen }) {
|
| 1711 |
+
const CancelGen = () => (
|
| 1712 |
+
<button type="button" onClick={onCancelGen}
|
| 1713 |
+
className="relative shrink-0 rounded-full border-2 border-primary/40 bg-card/85 px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-wide text-primary transition-colors hover:bg-accent hover:text-foreground">
|
| 1714 |
+
Cancel
|
| 1715 |
+
</button>
|
| 1716 |
+
);
|
| 1717 |
const [showPrompt, setShowPrompt] = useState(false);
|
| 1718 |
const [showTitlePrompt, setShowTitlePrompt] = useState(false);
|
| 1719 |
const [showTagsPrompt, setShowTagsPrompt] = useState(false);
|
|
|
|
| 1788 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "150ms" }} />
|
| 1789 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "300ms" }} />
|
| 1790 |
</span>
|
| 1791 |
+
<CancelGen />
|
| 1792 |
</div>
|
| 1793 |
) : null}
|
| 1794 |
</div>
|
|
|
|
| 1856 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "150ms" }} />
|
| 1857 |
<span className="size-1.5 rounded-full bg-primary" style={{ animation: "sozai-bounce 1s infinite", animationDelay: "300ms" }} />
|
| 1858 |
</span>
|
| 1859 |
+
<CancelGen />
|
| 1860 |
</div>
|
| 1861 |
) : null}
|
| 1862 |
</div>
|
|
|
|
| 2163 |
removingRef.current = false; // allow the next click immediately
|
| 2164 |
}
|
| 2165 |
|
| 2166 |
+
// A run of any auto-generation registers a token here. The photo browser
|
| 2167 |
+
// is locked while it's set (so a result can't land on the wrong photo),
|
| 2168 |
+
// and Cancel flips `cancelled` + clears the timers so the async result is
|
| 2169 |
+
// dropped instead of written back.
|
| 2170 |
+
const genRef = useRef(null);
|
| 2171 |
+
const genBusy = autoBusy || titleBusy || tagsBusy;
|
| 2172 |
+
function cancelGeneration() {
|
| 2173 |
+
const g = genRef.current;
|
| 2174 |
+
if (g) { g.cancelled = true; g.timers.forEach((t) => { clearInterval(t); clearTimeout(t); }); }
|
| 2175 |
+
genRef.current = null;
|
| 2176 |
+
setAutoBusy(false); setAutoStage("");
|
| 2177 |
+
setTitleBusy(false); setTitleStage("");
|
| 2178 |
+
setTagsBusy(false); setTagsStage("");
|
| 2179 |
+
}
|
| 2180 |
+
|
| 2181 |
function changePhoto(delta) {
|
| 2182 |
+
if (genBusy) return; // locked while developing
|
| 2183 |
if (stackPhotos.length <= 1) return; // nothing to browse
|
| 2184 |
setSlideDir(delta > 0 ? "right" : "left");
|
| 2185 |
setPhotoIndex((prev) => {
|
|
|
|
| 2214 |
if (!photo) return;
|
| 2215 |
if (window.sozaiPing) window.sozaiPing("✨ auto-generating caption");
|
| 2216 |
setAutoBusy(true); setAutoError(null);
|
| 2217 |
+
const token = { cancelled: false, timers: [] };
|
| 2218 |
+
genRef.current = token;
|
| 2219 |
let stageIdx = 0;
|
| 2220 |
setAutoStage(AUTO_STAGES[0]);
|
| 2221 |
const stageTimer = setInterval(() => {
|
| 2222 |
stageIdx = Math.min(stageIdx + 1, AUTO_STAGES.length - 1);
|
| 2223 |
setAutoStage(AUTO_STAGES[stageIdx]);
|
| 2224 |
}, 900);
|
| 2225 |
+
token.timers.push(stageTimer);
|
| 2226 |
const started = Date.now();
|
| 2227 |
const finish = (fn) => {
|
| 2228 |
// ensure the lovely animation is visible for at least a beat
|
| 2229 |
const wait = Math.max(0, 700 - (Date.now() - started));
|
| 2230 |
+
const t = setTimeout(() => {
|
| 2231 |
+
clearInterval(stageTimer);
|
| 2232 |
+
if (token.cancelled) return; // cancelled mid-flight: drop it
|
| 2233 |
+
fn(); setAutoBusy(false); setAutoStage("");
|
| 2234 |
+
if (genRef.current === token) genRef.current = null;
|
| 2235 |
+
}, wait);
|
| 2236 |
+
token.timers.push(t);
|
| 2237 |
};
|
| 2238 |
try {
|
| 2239 |
const gr = await window.__gradioReady;
|
|
|
|
| 2259 |
if (!photo) return;
|
| 2260 |
if (window.sozaiPing) window.sozaiPing("✨ auto-generating title");
|
| 2261 |
setTitleBusy(true); setTitleError(null);
|
| 2262 |
+
const token = { cancelled: false, timers: [] };
|
| 2263 |
+
genRef.current = token;
|
| 2264 |
let stageIdx = 0;
|
| 2265 |
setTitleStage(TITLE_STAGES[0]);
|
| 2266 |
const stageTimer = setInterval(() => {
|
| 2267 |
stageIdx = Math.min(stageIdx + 1, TITLE_STAGES.length - 1);
|
| 2268 |
setTitleStage(TITLE_STAGES[stageIdx]);
|
| 2269 |
}, 900);
|
| 2270 |
+
token.timers.push(stageTimer);
|
| 2271 |
const started = Date.now();
|
| 2272 |
const finish = (fn) => {
|
| 2273 |
const wait = Math.max(0, 700 - (Date.now() - started));
|
| 2274 |
+
const t = setTimeout(() => {
|
| 2275 |
+
clearInterval(stageTimer);
|
| 2276 |
+
if (token.cancelled) return;
|
| 2277 |
+
fn(); setTitleBusy(false); setTitleStage("");
|
| 2278 |
+
if (genRef.current === token) genRef.current = null;
|
| 2279 |
+
}, wait);
|
| 2280 |
+
token.timers.push(t);
|
| 2281 |
};
|
| 2282 |
// a title-specific default so a blank prompt still yields a *title*,
|
| 2283 |
// kept independent from the caption prompt.
|
|
|
|
| 2330 |
if (!photo) return;
|
| 2331 |
if (window.sozaiPing) window.sozaiPing("🏷️ auto-generating tags");
|
| 2332 |
setTagsBusy(true); setTagsError(null);
|
| 2333 |
+
const token = { cancelled: false, timers: [] };
|
| 2334 |
+
genRef.current = token;
|
| 2335 |
let stageIdx = 0;
|
| 2336 |
setTagsStage(TAGS_STAGES[0]);
|
| 2337 |
const stageTimer = setInterval(() => {
|
| 2338 |
stageIdx = Math.min(stageIdx + 1, TAGS_STAGES.length - 1);
|
| 2339 |
setTagsStage(TAGS_STAGES[stageIdx]);
|
| 2340 |
}, 900);
|
| 2341 |
+
token.timers.push(stageTimer);
|
| 2342 |
const started = Date.now();
|
| 2343 |
const finish = (fn) => {
|
| 2344 |
const wait = Math.max(0, 700 - (Date.now() - started));
|
| 2345 |
+
const t = setTimeout(() => {
|
| 2346 |
+
clearInterval(stageTimer);
|
| 2347 |
+
if (token.cancelled) return;
|
| 2348 |
+
fn(); setTagsBusy(false); setTagsStage("");
|
| 2349 |
+
if (genRef.current === token) genRef.current = null;
|
| 2350 |
+
}, wait);
|
| 2351 |
+
token.timers.push(t);
|
| 2352 |
};
|
| 2353 |
try {
|
| 2354 |
const gr = await window.__gradioReady;
|
|
|
|
| 2454 |
<input ref={addPhotoRef} type="file" accept={ACCEPT_ATTR} multiple className="hidden" onChange={onAddPhotos} />
|
| 2455 |
{stackPhotos.length >= 1 && (
|
| 2456 |
<div className="flex items-start gap-3">
|
| 2457 |
+
<button type="button" onClick={() => changePhoto(-1)} disabled={genBusy} aria-label="Previous photo" title={genBusy ? "Developing… cancel to browse" : "Previous photo"} className="sozai-sway-l flex size-9 items-center justify-center rounded-full border-2 border-primary/25 text-primary/55 transition-all hover:border-primary hover:bg-accent hover:text-primary disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:border-primary/25 disabled:hover:bg-transparent"><ChevronLeft className="size-4" /></button>
|
| 2458 |
<div className="flex flex-col items-center gap-1">
|
| 2459 |
<button type="button" onClick={() => addPhotoRef.current && addPhotoRef.current.click()} aria-label="Add photos" title="Add photos" className="flex size-9 items-center justify-center rounded-full border-2 border-primary bg-primary text-primary-foreground transition-colors hover:opacity-90"><Plus className="size-4" /></button>
|
| 2460 |
<span className="font-mono text-[9px] uppercase tracking-wide text-primary">upload</span>
|
| 2461 |
</div>
|
| 2462 |
<button type="button" onClick={onRemovePhoto} aria-label="Remove this photo" title="Remove this photo" className="flex size-9 items-center justify-center rounded-full border-2 border-[#800020] bg-[#800020] text-white transition-colors hover:opacity-90"><Trash2 className="size-4" /></button>
|
| 2463 |
+
<button type="button" onClick={() => changePhoto(1)} disabled={genBusy} aria-label="Next photo" title={genBusy ? "Developing… cancel to browse" : "Next photo"} className="sozai-sway-r flex size-9 items-center justify-center rounded-full border-2 border-primary/25 text-primary/55 transition-all hover:border-primary hover:bg-accent hover:text-primary disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:border-primary/25 disabled:hover:bg-transparent"><ChevronRight className="size-4" /></button>
|
| 2464 |
</div>
|
| 2465 |
)}
|
| 2466 |
{/* How this works - fills the left-column negative space */}
|
|
|
|
| 2487 |
onAutoTitle={handleAutoTitle} titleBusy={titleBusy} titleStage={titleStage} titleError={titleError}
|
| 2488 |
titlePrompt={settings.titlePrompt || ""} onTitlePromptChange={setTitlePrompt}
|
| 2489 |
onAutoTags={handleAutoTags} tagsBusy={tagsBusy} tagsStage={tagsStage} tagsError={tagsError}
|
| 2490 |
+
tagsPrompt={settings.tagsPrompt || ""} onTagsPromptChange={(v) => update({ tagsPrompt: v })}
|
| 2491 |
+
onCancelGen={cancelGeneration} />
|
| 2492 |
{validationError ? <p role="alert" className="rounded-md border-2 border-[oklch(0.55_0.18_25)]/50 bg-[oklch(0.55_0.18_25)]/10 px-3 py-2 text-center font-mono text-[11px] uppercase tracking-wide text-[oklch(0.5_0.18_25)]">{validationError}</p> : null}
|
| 2493 |
{confirmation ? <p role="status" className="rounded-md border-2 border-primary/40 bg-accent px-3 py-2 text-center text-sm text-accent-foreground">{confirmation}</p> : null}
|
| 2494 |
|
|
|
|
| 2545 |
<button type="button" onClick={() => onSelect && onSelect(moment.id)} style={{ left: `${moment.x}%`, top: `${moment.y}%` }}
|
| 2546 |
className={cn("group absolute z-10 w-44 -translate-x-1/2 -translate-y-1/2 rounded-sm border-2 border-primary bg-card p-2 text-left shadow-md transition-transform hover:z-20 hover:scale-105 sm:w-52", rotate, selected && "z-20 ring-4 ring-primary/30")}>
|
| 2547 |
<span aria-hidden="true" className="absolute -top-2.5 left-1/2 h-4 w-12 -translate-x-1/2 -rotate-2 rounded-sm bg-primary/15" />
|
|
|
|
| 2548 |
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-sm border border-primary/30"><Img src={moment.src} alt={moment.alt} fill sizes="208px" className="object-cover [filter:saturate(0.85)_contrast(1.05)]" /></div>
|
| 2549 |
<div className="px-0.5 pb-0.5 pt-2">
|
| 2550 |
<div className="flex items-start justify-between gap-1">
|