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:
@@ -31,6 +31,20 @@
|
||||
<img :src="c.thumbnail_url" alt="" loading="lazy" />
|
||||
</button>
|
||||
<div class="fc-ex__trail-actions">
|
||||
<!-- Reach (#1476): how far each step reaches past the anchor's cluster.
|
||||
Raise it to break out of a dense signature without hitting Random. -->
|
||||
<div
|
||||
class="fc-ex__reach"
|
||||
title="How far each step reaches — raise it to escape a dense cluster without going fully random"
|
||||
>
|
||||
<v-icon size="16" color="accent">mdi-map-marker-distance</v-icon>
|
||||
<v-slider
|
||||
:model-value="store.reach" @end="store.setReach"
|
||||
:min="0" :max="1" :step="0.2" hide-details density="compact"
|
||||
color="accent" class="fc-ex__reach-slider"
|
||||
/>
|
||||
<span class="fc-muted fc-ex__reach-label">{{ reachLabel }}</span>
|
||||
</div>
|
||||
<!-- Active retrain right where you tag: fold the +/- you just gave
|
||||
into the heads without a trip to Settings (the nightly beat is the
|
||||
passive cadence). -->
|
||||
@@ -153,6 +167,12 @@ const modal = useModalStore()
|
||||
|
||||
const anchorId = computed(() => route.params.imageId || null)
|
||||
const isVideo = computed(() => !!store.anchor?.mime?.startsWith('video/'))
|
||||
const reachLabel = computed(() => {
|
||||
const r = store.reach
|
||||
if (r <= 0.15) return 'Near'
|
||||
if (r <= 0.55) return 'Varied'
|
||||
return 'Far'
|
||||
})
|
||||
|
||||
// #1206: hovering a suggestion in the rail highlights the crop it came from on
|
||||
// the anchor image (same provide/inject as the modal viewer).
|
||||
@@ -296,6 +316,14 @@ onUnmounted(() => {
|
||||
.fc-ex__trail-actions {
|
||||
margin-left: auto; display: flex; align-items: center; gap: 4px; flex: 0 0 auto;
|
||||
}
|
||||
.fc-ex__reach {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.fc-ex__reach-slider { width: 96px; }
|
||||
.fc-ex__reach-label {
|
||||
font-size: 12px; min-width: 44px; text-align: left;
|
||||
}
|
||||
|
||||
/* The three panes fill the remaining height; each scrolls on its own.
|
||||
grid-template-rows: minmax(0, 1fr) BOUNDS the single row to the container
|
||||
|
||||
Reference in New Issue
Block a user