From 301f2de989efce6bf895459ee2b962a53cca4017 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 29 Jun 2026 20:44:16 -0400 Subject: [PATCH] =?UTF-8?q?fix(explore):=20variance=20+=20no=20loop-back?= =?UTF-8?q?=20on=20=E2=86=92=20navigation=20(#94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two reports: → sometimes "loops back", and the walk gets stuck on near-identical images. Cause: forwardTarget picked a uniformly-random neighbour from the 24 NEAREST, so it (a) often landed on an image already in the trail — which snaps the cursor back into history and makes → bounce between visited nodes — and (b) only ever offered near-duplicates. forwardTarget now: excludes already-visited neighbours (→ opens something new, no snap-back), and skips the closest third of the (similarity-sorted) pool so the jump favours the more-varied remainder instead of lookalikes. Neighbour pool widened 24→40 for more variety to browse + jump into. The post-← browser-forward walk through visited crumbs is unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa --- frontend/src/stores/explore.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/src/stores/explore.js b/frontend/src/stores/explore.js index 29f7cc0..1bb269f 100644 --- a/frontend/src/stores/explore.js +++ b/frontend/src/stores/explore.js @@ -11,7 +11,7 @@ import { toast } from '../utils/toast.js' // trail. The store ALSO acts as a TagPanel "host" (current/currentImageId + // tag CRUD over the anchor) so the Explore workspace reuses the modal's tag // rail verbatim for modal-parity tagging while rabbit-holing. -const NEIGHBOR_LIMIT = 24 +const NEIGHBOR_LIMIT = 40 // a wider pool → more variety to browse + jump into export const useExploreStore = defineStore('explore', () => { const api = useApi() @@ -81,16 +81,26 @@ export const useExploreStore = defineStore('explore', () => { return cursor.value > 0 ? breadcrumb.value[cursor.value - 1].id : null } - // → target: the next already-visited crumb if we'd stepped back, else a - // RANDOM neighbour to keep the rabbit-hole going. Null if neither exists. + // → target: after a ←, walk forward through the already-visited trail + // (browser-style). Otherwise jump to a varied neighbour to keep the + // rabbit-hole going — null if neither exists. function forwardTarget () { if (cursor.value >= 0 && cursor.value < breadcrumb.value.length - 1) { return breadcrumb.value[cursor.value + 1].id } - if (neighbors.value.length) { - return neighbors.value[Math.floor(Math.random() * neighbors.value.length)].id - } - return null + if (!neighbors.value.length) return null + // Prefer UNVISITED neighbours so → opens something new instead of landing on + // a crumb (which snaps the cursor back into the trail — the "loops back" + // report). Fall back to the full set only if every neighbour's been seen. + const seen = new Set(breadcrumb.value.map((c) => c.id)) + let pool = neighbors.value.filter((n) => !seen.has(n.id)) + if (!pool.length) pool = neighbors.value + // neighbors come similarity-sorted (nearest first). Skip the closest slice — + // those near-duplicates are exactly what you get stuck cycling through — and + // pick from the more-varied remainder, for real variance in the walk. + const skip = pool.length >= 6 ? Math.floor(pool.length / 3) : 0 + const cands = pool.slice(skip) + return cands[Math.floor(Math.random() * cands.length)].id } function reset () {