mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat: Add LoRA selector modal to Cycler widget
- Add LoraListModal component with search and preview tooltip - Make 'Next LoRA' name clickable to open selector modal - Integrate PreviewTooltip with custom resolver for Vue widgets - Disable selector when prompts are queued (consistent with pause button) - Fix tooltip z-index to display above modal backdrop Fixes issue: users couldn't easily identify which index corresponds to specific LoRA in large lists
This commit is contained in:
@@ -23,6 +23,15 @@
|
|||||||
@update:repeat-count="handleRepeatCountChange"
|
@update:repeat-count="handleRepeatCountChange"
|
||||||
@toggle-pause="handleTogglePause"
|
@toggle-pause="handleTogglePause"
|
||||||
@reset-index="handleResetIndex"
|
@reset-index="handleResetIndex"
|
||||||
|
@open-lora-selector="isModalOpen = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoraListModal
|
||||||
|
:visible="isModalOpen"
|
||||||
|
:lora-list="cachedLoraList"
|
||||||
|
:current-index="state.currentIndex.value"
|
||||||
|
@close="isModalOpen = false"
|
||||||
|
@select="handleModalSelect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,8 +39,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import LoraCyclerSettingsView from './lora-cycler/LoraCyclerSettingsView.vue'
|
import LoraCyclerSettingsView from './lora-cycler/LoraCyclerSettingsView.vue'
|
||||||
|
import LoraListModal from './lora-cycler/LoraListModal.vue'
|
||||||
import { useLoraCyclerState } from '../composables/useLoraCyclerState'
|
import { useLoraCyclerState } from '../composables/useLoraCyclerState'
|
||||||
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from '../composables/types'
|
import type { ComponentWidget, CyclerConfig, LoraPoolConfig, LoraItem } from '../composables/types'
|
||||||
|
|
||||||
type CyclerWidget = ComponentWidget<CyclerConfig>
|
type CyclerWidget = ComponentWidget<CyclerConfig>
|
||||||
|
|
||||||
@@ -86,6 +96,12 @@ const lastPoolConfigHash = ref('')
|
|||||||
// Track if component is mounted
|
// Track if component is mounted
|
||||||
const isMounted = ref(false)
|
const isMounted = ref(false)
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const isModalOpen = ref(false)
|
||||||
|
|
||||||
|
// Cache for LoRA list (used by modal)
|
||||||
|
const cachedLoraList = ref<LoraItem[]>([])
|
||||||
|
|
||||||
// Get pool config from connected node
|
// Get pool config from connected node
|
||||||
const getPoolConfig = (): LoraPoolConfig | null => {
|
const getPoolConfig = (): LoraPoolConfig | null => {
|
||||||
// Check if getPoolConfig method exists on node (added by main.ts)
|
// Check if getPoolConfig method exists on node (added by main.ts)
|
||||||
@@ -95,6 +111,17 @@ const getPoolConfig = (): LoraPoolConfig | null => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update display from LoRA list and index
|
||||||
|
const updateDisplayFromLoraList = (loraList: LoraItem[], index: number) => {
|
||||||
|
if (loraList.length > 0 && index > 0 && index <= loraList.length) {
|
||||||
|
const currentLora = loraList[index - 1]
|
||||||
|
if (currentLora) {
|
||||||
|
state.currentLoraName.value = currentLora.file_name
|
||||||
|
state.currentLoraFilename.value = currentLora.file_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle index update from user
|
// Handle index update from user
|
||||||
const handleIndexUpdate = async (newIndex: number) => {
|
const handleIndexUpdate = async (newIndex: number) => {
|
||||||
// Reset execution state when user manually changes index
|
// Reset execution state when user manually changes index
|
||||||
@@ -113,19 +140,18 @@ const handleIndexUpdate = async (newIndex: number) => {
|
|||||||
try {
|
try {
|
||||||
const poolConfig = getPoolConfig()
|
const poolConfig = getPoolConfig()
|
||||||
const loraList = await state.fetchCyclerList(poolConfig)
|
const loraList = await state.fetchCyclerList(poolConfig)
|
||||||
|
cachedLoraList.value = loraList
|
||||||
if (loraList.length > 0 && newIndex > 0 && newIndex <= loraList.length) {
|
updateDisplayFromLoraList(loraList, newIndex)
|
||||||
const currentLora = loraList[newIndex - 1]
|
|
||||||
if (currentLora) {
|
|
||||||
state.currentLoraName.value = currentLora.file_name
|
|
||||||
state.currentLoraFilename.value = currentLora.file_name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LoraCyclerWidget] Error updating index:', error)
|
console.error('[LoraCyclerWidget] Error updating index:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle LoRA selection from modal
|
||||||
|
const handleModalSelect = (index: number) => {
|
||||||
|
handleIndexUpdate(index)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle use custom clip range toggle
|
// Handle use custom clip range toggle
|
||||||
const handleUseCustomClipRangeChange = (newValue: boolean) => {
|
const handleUseCustomClipRangeChange = (newValue: boolean) => {
|
||||||
state.useCustomClipRange.value = newValue
|
state.useCustomClipRange.value = newValue
|
||||||
@@ -166,14 +192,8 @@ const handleResetIndex = async () => {
|
|||||||
try {
|
try {
|
||||||
const poolConfig = getPoolConfig()
|
const poolConfig = getPoolConfig()
|
||||||
const loraList = await state.fetchCyclerList(poolConfig)
|
const loraList = await state.fetchCyclerList(poolConfig)
|
||||||
|
cachedLoraList.value = loraList
|
||||||
if (loraList.length > 0) {
|
updateDisplayFromLoraList(loraList, 1)
|
||||||
const currentLora = loraList[0]
|
|
||||||
if (currentLora) {
|
|
||||||
state.currentLoraName.value = currentLora.file_name
|
|
||||||
state.currentLoraFilename.value = currentLora.file_name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LoraCyclerWidget] Error resetting index:', error)
|
console.error('[LoraCyclerWidget] Error resetting index:', error)
|
||||||
}
|
}
|
||||||
@@ -191,6 +211,9 @@ const checkPoolConfigChanges = async () => {
|
|||||||
lastPoolConfigHash.value = newHash
|
lastPoolConfigHash.value = newHash
|
||||||
try {
|
try {
|
||||||
await state.refreshList(poolConfig)
|
await state.refreshList(poolConfig)
|
||||||
|
// Update cached list when pool config changes
|
||||||
|
const loraList = await state.fetchCyclerList(poolConfig)
|
||||||
|
cachedLoraList.value = loraList
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LoraCyclerWidget] Error on pool config change:', error)
|
console.error('[LoraCyclerWidget] Error on pool config change:', error)
|
||||||
}
|
}
|
||||||
@@ -288,6 +311,9 @@ onMounted(async () => {
|
|||||||
const poolConfig = getPoolConfig()
|
const poolConfig = getPoolConfig()
|
||||||
lastPoolConfigHash.value = state.hashPoolConfig(poolConfig)
|
lastPoolConfigHash.value = state.hashPoolConfig(poolConfig)
|
||||||
await state.refreshList(poolConfig)
|
await state.refreshList(poolConfig)
|
||||||
|
// Cache the initial LoRA list for modal
|
||||||
|
const loraList = await state.fetchCyclerList(poolConfig)
|
||||||
|
cachedLoraList.value = loraList
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LoraCyclerWidget] Error on initial load:', error)
|
console.error('[LoraCyclerWidget] Error on initial load:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,18 @@
|
|||||||
<!-- Progress Display -->
|
<!-- Progress Display -->
|
||||||
<div class="setting-section progress-section">
|
<div class="setting-section progress-section">
|
||||||
<div class="progress-display" :class="{ executing: isWorkflowExecuting }">
|
<div class="progress-display" :class="{ executing: isWorkflowExecuting }">
|
||||||
<div class="progress-info">
|
<div
|
||||||
|
class="progress-info"
|
||||||
|
:class="{ disabled: isPauseDisabled }"
|
||||||
|
@click="handleOpenSelector"
|
||||||
|
>
|
||||||
<span class="progress-label">{{ isWorkflowExecuting ? 'Using LoRA:' : 'Next LoRA:' }}</span>
|
<span class="progress-label">{{ isWorkflowExecuting ? 'Using LoRA:' : 'Next LoRA:' }}</span>
|
||||||
<span class="progress-name" :title="currentLoraFilename">{{ currentLoraName || 'None' }}</span>
|
<span class="progress-name clickable" :class="{ disabled: isPauseDisabled }" :title="currentLoraFilename">
|
||||||
|
{{ currentLoraName || 'None' }}
|
||||||
|
<svg class="selector-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M7 10l5 5 5-5z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-counter">
|
<div class="progress-counter">
|
||||||
<span class="progress-index">{{ currentIndex }}</span>
|
<span class="progress-index">{{ currentIndex }}</span>
|
||||||
@@ -183,12 +192,20 @@ const emit = defineEmits<{
|
|||||||
'update:repeatCount': [value: number]
|
'update:repeatCount': [value: number]
|
||||||
'toggle-pause': []
|
'toggle-pause': []
|
||||||
'reset-index': []
|
'reset-index': []
|
||||||
|
'open-lora-selector': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Temporary value for input while typing
|
// Temporary value for input while typing
|
||||||
const tempIndex = ref<string>('')
|
const tempIndex = ref<string>('')
|
||||||
const tempRepeat = ref<string>('')
|
const tempRepeat = ref<string>('')
|
||||||
|
|
||||||
|
const handleOpenSelector = () => {
|
||||||
|
if (props.isPauseDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('open-lora-selector')
|
||||||
|
}
|
||||||
|
|
||||||
const onIndexInput = (event: Event) => {
|
const onIndexInput = (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement
|
const input = event.target as HTMLInputElement
|
||||||
tempIndex.value = input.value
|
tempIndex.value = input.value
|
||||||
@@ -313,6 +330,42 @@ const onRepeatBlur = (event: Event) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-name.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin: -2px -6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-name.clickable:hover:not(.disabled) {
|
||||||
|
background: rgba(66, 153, 225, 0.2);
|
||||||
|
color: rgba(191, 219, 254, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-name.clickable.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-name.clickable:hover .selector-icon {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-counter {
|
.progress-counter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
313
vue-widgets/src/components/lora-cycler/LoraListModal.vue
Normal file
313
vue-widgets/src/components/lora-cycler/LoraListModal.vue
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<ModalWrapper
|
||||||
|
:visible="visible"
|
||||||
|
title="Select LoRA"
|
||||||
|
:subtitle="subtitleText"
|
||||||
|
@close="$emit('close')"
|
||||||
|
>
|
||||||
|
<template #search>
|
||||||
|
<div class="search-container">
|
||||||
|
<svg class="search-icon" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
ref="searchInputRef"
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="Search LoRAs..."
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="searchQuery"
|
||||||
|
type="button"
|
||||||
|
class="clear-button"
|
||||||
|
@click="clearSearch"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="lora-list">
|
||||||
|
<div
|
||||||
|
v-for="item in filteredList"
|
||||||
|
:key="item.index"
|
||||||
|
class="lora-item"
|
||||||
|
:class="{ active: currentIndex === item.index }"
|
||||||
|
@mouseenter="showPreview(item.lora.file_name, $event)"
|
||||||
|
@mouseleave="hidePreview"
|
||||||
|
@click="selectLora(item.index)"
|
||||||
|
>
|
||||||
|
<span class="lora-index">{{ item.index }}</span>
|
||||||
|
<span class="lora-name" :title="item.lora.file_name">{{ item.lora.file_name }}</span>
|
||||||
|
<span v-if="currentIndex === item.index" class="current-badge">Current</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="filteredList.length === 0" class="no-results">
|
||||||
|
No LoRAs found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, nextTick, onUnmounted } from 'vue'
|
||||||
|
import ModalWrapper from '../lora-pool/modals/ModalWrapper.vue'
|
||||||
|
import type { LoraItem } from '../../composables/types'
|
||||||
|
|
||||||
|
interface LoraListItem {
|
||||||
|
index: number
|
||||||
|
lora: LoraItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
loraList: LoraItem[]
|
||||||
|
currentIndex: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
select: [index: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const searchInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
// Preview tooltip instance (lazy init)
|
||||||
|
let previewTooltip: any = null
|
||||||
|
|
||||||
|
const subtitleText = computed(() => {
|
||||||
|
const total = props.loraList.length
|
||||||
|
const filtered = filteredList.value.length
|
||||||
|
if (filtered === total) {
|
||||||
|
return `Total: ${total} LoRA${total !== 1 ? 's' : ''}`
|
||||||
|
}
|
||||||
|
return `Showing ${filtered} of ${total} LoRA${total !== 1 ? 's' : ''}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredList = computed<LoraListItem[]>(() => {
|
||||||
|
const list = props.loraList.map((lora, idx) => ({
|
||||||
|
index: idx + 1,
|
||||||
|
lora
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!searchQuery.value.trim()) {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.value.toLowerCase()
|
||||||
|
return list.filter(item =>
|
||||||
|
item.lora.file_name.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchQuery.value = ''
|
||||||
|
searchInputRef.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectLora = (index: number) => {
|
||||||
|
emit('select', index)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom preview URL resolver for Vue widgets environment
|
||||||
|
// The default preview_tooltip.js uses api.fetchApi which is mocked as native fetch
|
||||||
|
// in the Vue widgets build, so we need to use the full path with /api prefix
|
||||||
|
const customPreviewUrlResolver = async (modelName: string) => {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/lm/loras/preview-url?name=${encodeURIComponent(modelName)}&license_flags=true`
|
||||||
|
)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch preview URL')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
if (!data.success || !data.preview_url) {
|
||||||
|
throw new Error('No preview available')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
previewUrl: data.preview_url,
|
||||||
|
displayName: data.display_name ?? modelName,
|
||||||
|
licenseFlags: data.license_flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy load PreviewTooltip to avoid loading it unnecessarily
|
||||||
|
const getPreviewTooltip = async () => {
|
||||||
|
if (!previewTooltip) {
|
||||||
|
const { PreviewTooltip } = await import(/* @vite-ignore */ `${'../preview_tooltip.js'}`)
|
||||||
|
previewTooltip = new PreviewTooltip({
|
||||||
|
modelType: 'loras',
|
||||||
|
displayNameFormatter: (name: string) => name,
|
||||||
|
previewUrlResolver: customPreviewUrlResolver
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return previewTooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPreview = async (loraName: string, event: MouseEvent) => {
|
||||||
|
const tooltip = await getPreviewTooltip()
|
||||||
|
const rect = (event.target as HTMLElement).getBoundingClientRect()
|
||||||
|
// Position to the right of the item, centered vertically
|
||||||
|
tooltip.show(loraName, rect.right + 10, rect.top + rect.height / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePreview = async () => {
|
||||||
|
if (previewTooltip) {
|
||||||
|
previewTooltip.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus search input when modal opens
|
||||||
|
watch(() => props.visible, (isVisible) => {
|
||||||
|
if (isVisible) {
|
||||||
|
searchQuery.value = ''
|
||||||
|
nextTick(() => {
|
||||||
|
searchInputRef.value?.focus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Hide preview when modal closes
|
||||||
|
hidePreview()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (previewTooltip) {
|
||||||
|
previewTooltip.cleanup()
|
||||||
|
previewTooltip = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 32px;
|
||||||
|
background: var(--comfy-input-bg, #333);
|
||||||
|
border: 1px solid var(--border-color, #444);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: rgba(66, 153, 225, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-item:hover {
|
||||||
|
background: rgba(66, 153, 225, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-item.active {
|
||||||
|
background: rgba(66, 153, 225, 0.25);
|
||||||
|
border-left-color: rgba(66, 153, 225, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-index {
|
||||||
|
font-family: 'SF Mono', 'Roboto Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(226, 232, 240, 0.5);
|
||||||
|
min-width: 3ch;
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: rgba(66, 153, 225, 0.3);
|
||||||
|
border: 1px solid rgba(66, 153, 225, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: rgba(191, 219, 254, 1);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
padding: 32px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -81,7 +81,7 @@ watch(() => props.visible, (isVisible) => {
|
|||||||
.lora-pool-modal-backdrop {
|
.lora-pool-modal-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 10000;
|
z-index: 9998;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export default defineConfig({
|
|||||||
'../../../scripts/app.js',
|
'../../../scripts/app.js',
|
||||||
'../../../scripts/api.js',
|
'../../../scripts/api.js',
|
||||||
'../loras_widget.js',
|
'../loras_widget.js',
|
||||||
'../autocomplete.js'
|
'../autocomplete.js',
|
||||||
|
'../preview_tooltip.js'
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
dir: '../web/comfyui/vue-widgets',
|
dir: '../web/comfyui/vue-widgets',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* Shared styling for the LoRA Manager frontend widgets */
|
/* Shared styling for the LoRA Manager frontend widgets */
|
||||||
.lm-tooltip {
|
.lm-tooltip {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 10001;
|
||||||
background: rgba(0, 0, 0, 0.85);
|
background: rgba(0, 0, 0, 0.85);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user