diff --git a/VIDEO_IMAGE_REFS_INVESTIGATION.md b/VIDEO_IMAGE_REFS_INVESTIGATION.md new file mode 100644 index 0000000000..9d17ab1fa0 --- /dev/null +++ b/VIDEO_IMAGE_REFS_INVESTIGATION.md @@ -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] + → [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): +`/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. diff --git a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx index e7d690fc20..99d407add0 100644 --- a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx @@ -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); }, diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx index c8b1828703..406e069417 100644 --- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx +++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx @@ -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]);