Compare commits

...
Author SHA1 Message Date
Tanish Manku 283dd022b2 Investigation + diagnostics: video page "Image Ref" attach silently fails
Adds a continuity report (VIDEO_IMAGE_REFS_INVESTIGATION.md) documenting the webapp image-ref attach flow, what was verified correct, the git archaeology pointing at #1662 (motion pass) and a stuck galleryModalDraggingUnder signal, and a decision table for interpreting the diagnostics.

Adds temporary [IMGREF-DEBUG] logging at the two points where a selection can silently vanish: create-video.tsx handleLibraryImageSelect and gallery-modal.tsx handleUseSelected. To be removed once the fault class is confirmed.
2026-06-24 19:35:14 -07:00
3 changed files with 204 additions and 3 deletions
+173
View File
@@ -0,0 +1,173 @@
# Investigation: Video page "Image Ref" attach silently fails
**Branch:** `fix/video-image-refs` (worktree off `origin/main`, created to avoid
interfering with agents on `qol/settings-keybinds`).
**Status:** Root cause not yet confirmed — diagnostics added, awaiting a runtime
reproduction. Read this top-to-bottom to resume.
---
## 1. Symptom (as reported)
On the **video page**, in the **"Image Ref"** section (reference mode), attaching a
reference image from the library fails:
- The gallery modal opens and **shows** images.
- An image can be **selected**, and confirming **closes the modal**.
- But the reference image **never appears** in the promptbox.
- **No error logs** are fired.
**Confirmed app:** the web app — **`artcraft-webapp`** (not the Tauri desktop app,
not `artcraft-website`). The three apps each have their *own* copy of the
prompt-box code, so only the webapp path matters here.
---
## 2. The webapp attach flow (all verified correct on `main`)
```
ImagePromptRow "Pick from library"
→ onPickFromLibrary → setIsImagePickerOpen(true) [create-video.tsx]
→ <GalleryModal mode="select" .../> [create-video.tsx:1595]
click an item → handleItemClick → onSelectItem(item.id) [gallery-modal.tsx:1271]
→ handlePickerSelect → pickerSelectedIds += id [create-video.tsx:804]
"Use selected" → handleUseSelected [gallery-modal.tsx:1300]
→ Object.values(groupedItems).flat()
.filter(i => selectedItemIds.includes(i.id))
→ onUseSelected(selectedItems)
→ handleLibraryImageSelect(items) [create-video.tsx:825]
maxImages = isReferenceMode ? (image_references_max ?? 3) : 1
availableSlots = max(0, maxImages - referenceImages.length)
newImages = items.slice(0, availableSlots).map(... url: thumbnail||fullImage||"" ...)
setReferenceImages([...referenceImages, ...newImages]) → store (setRefs)
→ ImagePromptRow re-renders referenceImages tiles [prompt-box/ImagePromptRow.tsx]
```
### Key relevant files (webapp)
| Purpose | Path |
|---|---|
| Video page + attach handler | `frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx` |
| Image-ref row UI (renders tiles) | `frontend/apps/artcraft-webapp/src/components/prompt-box/ImagePromptRow.tsx` |
| PromptBox wrapper | `frontend/apps/artcraft-webapp/src/components/prompt-box/PromptBox.tsx` |
| Refs store (`setRefs`) | `frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts` |
| Shared gallery modal | `frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx` |
| Drag lifecycle / stuck-signal | `frontend/libs/components/gallery-modal/src/lib/galleryDnd.ts` |
| Modal `contentInteractive`/`contentHidden` | `frontend/libs/components/modal/src/lib/modal.tsx` |
### What was checked and ruled OUT as a trivial logic bug
- **Happy-path logic is correct** end-to-end for a normal image. Selection ids
match grouped-item ids; the handler maps and commits to the store; the store
merge (`setRefs`) is correct; `ImagePromptRow` renders `referenceImages`.
- **`fullImage` is reliably populated:** it maps to `media_links.cdn_url`, which
the Rust `MediaLinks` struct declares as a **required** `Url`
(`crates/api_clients/artcraft/artcraft_api_defs/src/common/responses/media_links.rs:10`).
So `url: thumbnail || fullImage || ""` won't produce an empty url for list items.
- **Branch vs main:** all the above webapp/lib files are **identical** between
`origin/main` and `qol/settings-keybinds` — so the bug is on `main`, and this
worktree reproduces the user's build.
Conclusion: this is a **runtime-specific failure that leaves no trace**, which is
why nothing logs.
---
## 3. Git archaeology — last code to touch the failure path
The attach logic and gallery selection/confirm core are **old & stable** (not the
regression):
- `handleLibraryImageSelect` — `11a2547beb` (#1503, 2026-05-12), one line `fa4776bb21` (#1532).
- `handleUseSelected` / `handleItemClick` — `f3b5a774` (2025-04), deps tweak `e8d88ca3` (2026-02).
**The only recent change to the actual click/visibility path:**
→ **`c5c68d301a` — "Webapp: Premium motion pass across app" (#1662), 2026-06-23.**
It wrapped the gallery `Modal` in new gating (`gallery-modal.tsx:2148`):
```jsx
contentInteractive={!galleryModalDraggingUnder.value} // false ⇒ pointerEvents:"none" (modal.tsx:915)
contentDimmed ={galleryModalDraggingUnder.value && galleryReopenAfterDragSignal.value} // ⇒ opacity 0.4
contentHidden ={galleryModalDraggingUnder.value && !galleryReopenAfterDragSignal.value} // ⇒ opacity 0 (modal.tsx:283)
```
(The newest commit on `gallery-modal.tsx` overall, `61a0880c80` #1661 Moodboard,
only changed the logged-out "Log in" button — not this path.)
### Concrete defect found inside #1662 (candidate root cause)
`galleryModalDraggingUnder` is a **global signal**. Its reset is conditional
(`galleryDnd.ts:273`):
```js
if (!closedHidden) {
galleryModalDraggingUnder.value = false; // SKIPPED when an image is dropped on
} // canvas with "reopen after adding" OFF
```
When skipped the signal **stays `true`**, and — despite the code comment claiming
it is "reset on open" — **there is no reset-on-open anywhere**. Grep of every
usage: set `true` at `galleryDnd.ts:167`, set `false` only at `:274`, default
`false` (`galleryModalSignals.ts:11`). Nothing clears it when a modal opens.
A stuck-`true` value makes the **next** gallery to open (including the webapp's
image-ref picker, since the signal is global) render with `pointerEvents:none`
and/or `opacity:0`.
**Caveat (why this may not be the whole story):** triggering the stuck state
requires a *prior* drag-to-canvas with reopen-off. That doesn't cleanly match the
report that the gallery "shows and selects fine." So #1662 is unambiguously the
last code to touch this path and contains a real bug, but it is **not yet
confirmed** to be the exact cause of the reported symptom. Diagnostics (below)
will decide.
---
## 4. Diagnostics added in this commit (temporary — remove after)
Both marked `[IMGREF-DEBUG]`:
1. **`create-video.tsx` → `handleLibraryImageSelect`** — logs received item
count + each item's `{id, fullImage, thumbnail, mediaClass}`, `isReferenceMode`,
`maxImages`, `currentRefCount`, `availableSlots`, `newImageCount`, `nextRefCount`.
2. **`gallery-modal.tsx` → `handleUseSelected`** — logs `selectedItemIds`,
`groupedFlatCount`, `matchedCount`, `hasOnUseSelected`.
A copy of the patch is also at (regenerate with `git diff` if lost):
`<scratchpad>/imgref-debug.patch`
### How to reproduce + capture
```
cd frontend
pnpm install
npx nx serve artcraft-webapp # or the usual webapp dev command
```
DevTools console → filter `[IMGREF-DEBUG]` → video page → reference mode →
**Image Ref** → Pick from library → select 1 image → **Use selected**.
### Decision table (maps output → fault class)
| Console output | Root-cause class |
|---|---|
| **No `gallery.handleUseSelected` line at all** | Click never reaches handler — event/pointer interception. **Prime suspect: #1662 `contentInteractive`/`contentHidden` stuck via `galleryModalDraggingUnder`.** |
| `handleUseSelected` logs, `matchedCount: 0` | Selected ids don't match grouped items — selection-state bug. |
| `handleLibraryImageSelect` logs, `newImageCount: 0` | Slot math drops it — check `isReferenceMode`/`maxImages`/`image_references_max`. |
| Both log, `newImageCount ≥ 1`, `nextRefCount ≥ 1`, **but no tile** | Pure render/visibility bug — store updates but tile doesn't show. |
---
## 5. Candidate fix (if logs confirm the stuck-signal class)
Add a reset-on-open for the global signal so a stale `true` can't carry into the
next gallery open. Options:
- In `gallery-modal.tsx`, an effect: when the modal transitions to open
(`isOpen` true / view-mode visible), set `galleryModalDraggingUnder.value = false`.
- Or unconditionally reset in `galleryDnd.ts` `onPointerUp` and instead drive the
"stay faded through close" purely from the close animation (decouple the two
concerns so cleanup is never skipped).
Verify after: drag an image to canvas with "reopen after adding" OFF, then open
the image-ref picker — it must be fully visible and interactive.
---
## 6. Resume checklist
- [ ] Run webapp from this worktree, capture `[IMGREF-DEBUG]` output.
- [ ] Match output to the §4 decision table → fault class.
- [ ] If stuck-signal: apply §5 fix. Else follow the matched class.
- [ ] Remove both `[IMGREF-DEBUG]` blocks before final PR.
- [ ] Manually verify the reference image appears in the promptbox after attach.
@@ -837,6 +837,24 @@ export default function CreateVideo() {
file: new File([], "library-image"),
mediaToken: item.id,
}));
// [IMGREF-DEBUG] temporary diagnostic — remove once root cause is found.
// Tells us whether onUseSelected fired, what items it received, the slot
// math, and what we're about to commit to the store.
console.log("[IMGREF-DEBUG] handleLibraryImageSelect", {
receivedItemCount: items.length,
items: items.map((it) => ({
id: it.id,
fullImage: it.fullImage,
thumbnail: it.thumbnail,
mediaClass: (it as { mediaClass?: string }).mediaClass,
})),
isReferenceMode,
maxImages,
currentRefCount: referenceImages.length,
availableSlots,
newImageCount: newImages.length,
nextRefCount: referenceImages.length + newImages.length,
});
setReferenceImages([...referenceImages, ...newImages]);
setIsImagePickerOpen(false);
},
@@ -1298,9 +1298,19 @@ export const GalleryModal = React.memo(
}, [selectedItemIds, onSelectItem]);
const handleUseSelected = useCallback(() => {
const selectedItems = Object.values(groupedItems)
.flat()
.filter((item) => selectedItemIds.includes(item.id));
const flat = Object.values(groupedItems).flat();
const selectedItems = flat.filter((item) =>
selectedItemIds.includes(item.id),
);
// [IMGREF-DEBUG] temporary diagnostic — remove once root cause is found.
// Distinguishes "confirm button never reached onUseSelected" from
// "confirm fired but selectedIds didn't match any grouped item".
console.log("[IMGREF-DEBUG] gallery.handleUseSelected", {
selectedItemIds,
groupedFlatCount: flat.length,
matchedCount: selectedItems.length,
hasOnUseSelected: !!onUseSelected,
});
onUseSelected?.(selectedItems);
}, [groupedItems, selectedItemIds, onUseSelected]);