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
Files changed (1) hide show
  1. 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(() => { clearInterval(stageTimer); fn(); setAutoBusy(false); setAutoStage(""); }, wait);
 
 
 
 
 
 
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(() => { clearInterval(stageTimer); fn(); setTitleBusy(false); setTitleStage(""); }, wait);
 
 
 
 
 
 
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(() => { clearInterval(stageTimer); fn(); setTagsBusy(false); setTagsStage(""); }, wait);
 
 
 
 
 
 
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">