fix: Show all tags in LoRA Pool without limit (#819)

- Backend: Support limit=0 to return all tags in top-tags API
- Frontend: Remove tags limit setting and fetch all tags by default
- UI: Implement virtual scrolling in TagsModal for performance
  - Initial display 200 tags, load more on scroll
  - Show all results when searching
- Remove lora_pool_tags_limit setting to simplify UX

Fixes #819
This commit is contained in:
Will Miao
2026-02-19 09:59:08 +08:00
parent b9516c6b62
commit e8b37365a6
6 changed files with 144 additions and 36 deletions

View File

@@ -31,9 +31,9 @@
</div>
</template>
<div class="tags-container">
<div ref="tagsContainerRef" class="tags-container" @scroll="handleScroll">
<button
v-for="tag in filteredTags"
v-for="tag in visibleTags"
:key="tag.tag"
type="button"
class="tag-chip"
@@ -42,9 +42,12 @@
>
{{ tag.tag }}
</button>
<div v-if="filteredTags.length === 0" class="no-results">
<div v-if="visibleTags.length === 0" class="no-results">
No tags found
</div>
<div v-if="hasMoreTags" class="load-more-hint">
Scroll to load more...
</div>
</div>
</ModalWrapper>
</template>
@@ -78,6 +81,11 @@ const subtitle = computed(() =>
const searchQuery = ref('')
const searchInputRef = ref<HTMLInputElement | null>(null)
const tagsContainerRef = ref<HTMLElement | null>(null)
const displayedCount = ref(200)
const BATCH_SIZE = 200
const SCROLL_THRESHOLD = 100
const filteredTags = computed(() => {
if (!searchQuery.value) {
@@ -87,6 +95,20 @@ const filteredTags = computed(() => {
return props.tags.filter(t => t.tag.toLowerCase().includes(query))
})
const visibleTags = computed(() => {
// When searching, show all filtered results
if (searchQuery.value) {
return filteredTags.value
}
// Otherwise, use virtual scrolling
return filteredTags.value.slice(0, displayedCount.value)
})
const hasMoreTags = computed(() => {
if (searchQuery.value) return false
return displayedCount.value < filteredTags.value.length
})
const isSelected = (tag: string) => {
return props.selected.includes(tag)
}
@@ -100,16 +122,40 @@ const toggleTag = (tag: string) => {
const clearSearch = () => {
searchQuery.value = ''
displayedCount.value = BATCH_SIZE
searchInputRef.value?.focus()
}
const handleScroll = () => {
if (searchQuery.value) return
const container = tagsContainerRef.value
if (!container) return
const { scrollTop, scrollHeight, clientHeight } = container
const scrollBottom = scrollHeight - scrollTop - clientHeight
// Load more tags when user scrolls near bottom
if (scrollBottom < SCROLL_THRESHOLD && hasMoreTags.value) {
displayedCount.value = Math.min(
displayedCount.value + BATCH_SIZE,
filteredTags.value.length
)
}
}
watch(() => props.visible, (isVisible) => {
if (isVisible) {
displayedCount.value = BATCH_SIZE
nextTick(() => {
searchInputRef.value?.focus()
})
}
})
watch(() => props.tags, () => {
displayedCount.value = BATCH_SIZE
})
</script>
<style scoped>
@@ -245,4 +291,14 @@ watch(() => props.visible, (isVisible) => {
opacity: 0.5;
font-size: 13px;
}
.load-more-hint {
width: 100%;
padding: 12px;
text-align: center;
color: var(--fg-color, #fff);
opacity: 0.4;
font-size: 12px;
font-style: italic;
}
</style>

View File

@@ -15,7 +15,7 @@ export function useLoraPoolApi() {
}
}
const fetchTags = async (limit = 100): Promise<TagOption[]> => {
const fetchTags = async (limit = 0): Promise<TagOption[]> => {
try {
const response = await fetch(`/api/lm/loras/top-tags?limit=${limit}`)
const data = await response.json()