feat(explore): reach dial to escape dense clusters + anti-revisit (#1476)
The Explore walk got stuck in dense signatures — neighbours all too similar, so forward-arrow couldn't escape and Random was the only exit. Root cause: MMR only diversifies WITHIN the nearest ~400 pool; in a dense cluster that whole pool is near-identical, so there's no escape route in it. - gallery_service.similar(reach=0.0, exclude_ids=None): reach>0 widens the pool (cap 400→1000) and _reach_sample strides across an outward-growing distance span so the set handed to MMR spans near→mid-far (guaranteed escape routes), not just the tight cluster. exclude_ids drops already-walked images. Gallery 'more like this' (reach=0) is unchanged. - api/gallery similar: parse reach + exclude_ids. - explore store: default reach 0.4 (auto-diversifies without touching the dial), pass the breadcrumb as exclude_ids, setReach action. - ExploreView: a Near↔Far reach slider in the trail. - tests: _reach_sample math (deeper ranks with higher reach, near kept); similar exclude_ids drops walked + reach path runs clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,10 @@ export const useExploreStore = defineStore('explore', () => {
|
||||
const cursor = ref(-1)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
// Reach (#1476): how far the walk reaches past the anchor's immediate cluster.
|
||||
// 0 = nearest (can get stuck in a dense signature); ~0.4 default mixes in
|
||||
// mid-far escape routes so the walk diversifies without hitting "Random image".
|
||||
const reach = ref(0.4)
|
||||
|
||||
const inflight = useInflightToken()
|
||||
|
||||
@@ -46,7 +50,13 @@ export const useExploreStore = defineStore('explore', () => {
|
||||
const body = await api.get('/api/gallery/similar', {
|
||||
// exclude_wip: keep work-in-progress out of the Explore rabbit-hole
|
||||
// (the gallery's own "similar" button still shows it) — operator 2026-07-08.
|
||||
params: { similar_to: numId, limit: NEIGHBOR_LIMIT, exclude_wip: 1 },
|
||||
// reach + exclude_ids (#1476): reach past the dense cluster + never re-serve
|
||||
// an already-walked image, so the walk keeps moving instead of getting stuck.
|
||||
params: {
|
||||
similar_to: numId, limit: NEIGHBOR_LIMIT, exclude_wip: 1,
|
||||
reach: reach.value,
|
||||
exclude_ids: breadcrumb.value.map((c) => c.id).join(','),
|
||||
},
|
||||
})
|
||||
if (!t.isCurrent()) return
|
||||
neighbors.value = body.images || []
|
||||
@@ -113,6 +123,14 @@ export const useExploreStore = defineStore('explore', () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// Change how far the walk reaches and re-fetch the current anchor's neighbours
|
||||
// with the new setting (the anchor + trail are unchanged — only the grid varies).
|
||||
function setReach (v) {
|
||||
reach.value = Math.max(0, Math.min(1, Number(v)))
|
||||
const id = anchor.value?.id
|
||||
if (id != null) anchorOn(id)
|
||||
}
|
||||
|
||||
// --- TagPanel "host" surface ---------------------------------------------
|
||||
// The anchor IS the current image (same /api/gallery/image/<id> payload the
|
||||
// modal uses), so these mirror the modal store's tag-CRUD, targeting the
|
||||
@@ -189,8 +207,8 @@ export const useExploreStore = defineStore('explore', () => {
|
||||
function close () {}
|
||||
|
||||
return {
|
||||
anchor, neighbors, breadcrumb, cursor, loading, error, NEIGHBOR_LIMIT,
|
||||
anchorOn, reset, backTarget, forwardTarget,
|
||||
anchor, neighbors, breadcrumb, cursor, loading, error, NEIGHBOR_LIMIT, reach,
|
||||
anchorOn, reset, backTarget, forwardTarget, setReach,
|
||||
// host surface
|
||||
current, currentImageId,
|
||||
reloadTags, addExistingTag, removeTag, createAndAdd, close,
|
||||
|
||||
Reference in New Issue
Block a user