mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat(randomizer): add lora pool Vue widget
This commit is contained in:
59
vue-widgets/src/composables/types.ts
Normal file
59
vue-widgets/src/composables/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Shared types for LoRA Pool Widget
|
||||
|
||||
export interface LoraPoolConfig {
|
||||
version: number
|
||||
filters: {
|
||||
baseModels: string[]
|
||||
tags: { include: string[]; exclude: string[] }
|
||||
folders: { include: string[]; exclude: string[] }
|
||||
license: {
|
||||
noCreditRequired: boolean
|
||||
allowSelling: boolean
|
||||
}
|
||||
}
|
||||
preview: { matchCount: number; lastUpdated: number }
|
||||
}
|
||||
|
||||
export interface LoraItem {
|
||||
file_path: string
|
||||
file_name: string
|
||||
model_name?: string
|
||||
preview_url?: string
|
||||
}
|
||||
|
||||
export interface BaseModelOption {
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface TagOption {
|
||||
tag: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface FolderTreeNode {
|
||||
key: string
|
||||
label: string
|
||||
children?: FolderTreeNode[]
|
||||
}
|
||||
|
||||
export interface ComponentWidget {
|
||||
serializeValue?: () => Promise<LoraPoolConfig>
|
||||
value?: LoraPoolConfig
|
||||
}
|
||||
|
||||
// Legacy config for migration (v1)
|
||||
export interface LegacyLoraPoolConfig {
|
||||
version: 1
|
||||
filters: {
|
||||
baseModels: string[]
|
||||
tags: { include: string[]; exclude: string[] }
|
||||
folder: { path: string | null; recursive: boolean }
|
||||
favoritesOnly: boolean
|
||||
license: {
|
||||
noCreditRequired: boolean | null
|
||||
allowSellingGeneratedContent: boolean | null
|
||||
}
|
||||
}
|
||||
preview: { matchCount: number; lastUpdated: number }
|
||||
}
|
||||
116
vue-widgets/src/composables/useLoraPoolApi.ts
Normal file
116
vue-widgets/src/composables/useLoraPoolApi.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { ref } from 'vue'
|
||||
import type { BaseModelOption, TagOption, FolderTreeNode, LoraItem } from './types'
|
||||
|
||||
export function useLoraPoolApi() {
|
||||
const isLoading = ref(false)
|
||||
|
||||
const fetchBaseModels = async (limit = 50): Promise<BaseModelOption[]> => {
|
||||
try {
|
||||
const response = await fetch(`/api/lm/loras/base-models?limit=${limit}`)
|
||||
const data = await response.json()
|
||||
return data.base_models || []
|
||||
} catch (error) {
|
||||
console.error('[LoraPoolApi] Failed to fetch base models:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTags = async (limit = 100): Promise<TagOption[]> => {
|
||||
try {
|
||||
const response = await fetch(`/api/lm/loras/top-tags?limit=${limit}`)
|
||||
const data = await response.json()
|
||||
return data.tags || []
|
||||
} catch (error) {
|
||||
console.error('[LoraPoolApi] Failed to fetch tags:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const fetchFolderTree = async (): Promise<FolderTreeNode[]> => {
|
||||
try {
|
||||
const response = await fetch('/api/lm/loras/unified-folder-tree')
|
||||
const data = await response.json()
|
||||
return transformFolderTree(data.tree || {})
|
||||
} catch (error) {
|
||||
console.error('[LoraPoolApi] Failed to fetch folder tree:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const transformFolderTree = (tree: Record<string, any>, parentPath = ''): FolderTreeNode[] => {
|
||||
if (!tree || typeof tree !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(tree).map(([name, children]) => {
|
||||
const path = parentPath ? `${parentPath}/${name}` : name
|
||||
const childNodes = transformFolderTree(children as Record<string, any>, path)
|
||||
|
||||
return {
|
||||
key: path,
|
||||
label: name,
|
||||
children: childNodes.length > 0 ? childNodes : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface FetchLorasParams {
|
||||
baseModels?: string[]
|
||||
tagsInclude?: string[]
|
||||
tagsExclude?: string[]
|
||||
foldersInclude?: string[]
|
||||
foldersExclude?: string[]
|
||||
noCreditRequired?: boolean
|
||||
allowSelling?: boolean
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
const fetchLoras = async (params: FetchLorasParams): Promise<{ items: LoraItem[]; total: number }> => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const urlParams = new URLSearchParams()
|
||||
urlParams.set('page', String(params.page || 1))
|
||||
urlParams.set('page_size', String(params.pageSize || 6))
|
||||
|
||||
params.baseModels?.forEach(bm => urlParams.append('base_model', bm))
|
||||
params.tagsInclude?.forEach(tag => urlParams.append('tag_include', tag))
|
||||
params.tagsExclude?.forEach(tag => urlParams.append('tag_exclude', tag))
|
||||
|
||||
// For now, use first include folder (backend currently supports single folder)
|
||||
if (params.foldersInclude && params.foldersInclude.length > 0) {
|
||||
urlParams.set('folder', params.foldersInclude[0])
|
||||
urlParams.set('recursive', 'true')
|
||||
}
|
||||
|
||||
if (params.noCreditRequired !== undefined) {
|
||||
urlParams.set('credit_required', String(!params.noCreditRequired))
|
||||
}
|
||||
|
||||
if (params.allowSelling !== undefined) {
|
||||
urlParams.set('allow_selling_generated_content', String(params.allowSelling))
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/lm/loras/list?${urlParams}`)
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
items: data.items || [],
|
||||
total: data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LoraPoolApi] Failed to fetch loras:', error)
|
||||
return { items: [], total: 0 }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
fetchBaseModels,
|
||||
fetchTags,
|
||||
fetchFolderTree,
|
||||
fetchLoras
|
||||
}
|
||||
}
|
||||
187
vue-widgets/src/composables/useLoraPoolState.ts
Normal file
187
vue-widgets/src/composables/useLoraPoolState.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type {
|
||||
LoraPoolConfig,
|
||||
LegacyLoraPoolConfig,
|
||||
BaseModelOption,
|
||||
TagOption,
|
||||
FolderTreeNode,
|
||||
LoraItem,
|
||||
ComponentWidget
|
||||
} from './types'
|
||||
import { useLoraPoolApi } from './useLoraPoolApi'
|
||||
|
||||
export function useLoraPoolState(widget: ComponentWidget) {
|
||||
const api = useLoraPoolApi()
|
||||
|
||||
// Filter state
|
||||
const selectedBaseModels = ref<string[]>([])
|
||||
const includeTags = ref<string[]>([])
|
||||
const excludeTags = ref<string[]>([])
|
||||
const includeFolders = ref<string[]>([])
|
||||
const excludeFolders = ref<string[]>([])
|
||||
const noCreditRequired = ref(false)
|
||||
const allowSelling = ref(false)
|
||||
|
||||
// Available options from API
|
||||
const availableBaseModels = ref<BaseModelOption[]>([])
|
||||
const availableTags = ref<TagOption[]>([])
|
||||
const folderTree = ref<FolderTreeNode[]>([])
|
||||
|
||||
// Preview state
|
||||
const previewItems = ref<LoraItem[]>([])
|
||||
const matchCount = ref(0)
|
||||
const isLoading = computed(() => api.isLoading.value)
|
||||
|
||||
// Build config from current state
|
||||
const buildConfig = (): LoraPoolConfig => {
|
||||
const config: LoraPoolConfig = {
|
||||
version: 2,
|
||||
filters: {
|
||||
baseModels: selectedBaseModels.value,
|
||||
tags: {
|
||||
include: includeTags.value,
|
||||
exclude: excludeTags.value
|
||||
},
|
||||
folders: {
|
||||
include: includeFolders.value,
|
||||
exclude: excludeFolders.value
|
||||
},
|
||||
license: {
|
||||
noCreditRequired: noCreditRequired.value,
|
||||
allowSelling: allowSelling.value
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
matchCount: matchCount.value,
|
||||
lastUpdated: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
// Update widget value
|
||||
widget.value = config
|
||||
return config
|
||||
}
|
||||
|
||||
// Migrate legacy config (v1) to current format (v2)
|
||||
const migrateConfig = (legacy: LegacyLoraPoolConfig): LoraPoolConfig => {
|
||||
return {
|
||||
version: 2,
|
||||
filters: {
|
||||
baseModels: legacy.filters.baseModels || [],
|
||||
tags: {
|
||||
include: legacy.filters.tags?.include || [],
|
||||
exclude: legacy.filters.tags?.exclude || []
|
||||
},
|
||||
folders: {
|
||||
include: legacy.filters.folder?.path ? [legacy.filters.folder.path] : [],
|
||||
exclude: []
|
||||
},
|
||||
license: {
|
||||
noCreditRequired: legacy.filters.license?.noCreditRequired ?? false,
|
||||
allowSelling: legacy.filters.license?.allowSellingGeneratedContent ?? false
|
||||
}
|
||||
},
|
||||
preview: legacy.preview || { matchCount: 0, lastUpdated: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Restore state from config
|
||||
const restoreFromConfig = (rawConfig: LoraPoolConfig | LegacyLoraPoolConfig) => {
|
||||
// Migrate if needed
|
||||
const config = rawConfig.version === 1
|
||||
? migrateConfig(rawConfig as LegacyLoraPoolConfig)
|
||||
: rawConfig as LoraPoolConfig
|
||||
|
||||
if (!config?.filters) return
|
||||
|
||||
const { filters, preview } = config
|
||||
selectedBaseModels.value = filters.baseModels || []
|
||||
includeTags.value = filters.tags?.include || []
|
||||
excludeTags.value = filters.tags?.exclude || []
|
||||
includeFolders.value = filters.folders?.include || []
|
||||
excludeFolders.value = filters.folders?.exclude || []
|
||||
noCreditRequired.value = filters.license?.noCreditRequired ?? false
|
||||
allowSelling.value = filters.license?.allowSelling ?? false
|
||||
matchCount.value = preview?.matchCount || 0
|
||||
}
|
||||
|
||||
// Fetch filter options from API
|
||||
const fetchFilterOptions = async () => {
|
||||
const [baseModels, tags, folders] = await Promise.all([
|
||||
api.fetchBaseModels(),
|
||||
api.fetchTags(),
|
||||
api.fetchFolderTree()
|
||||
])
|
||||
|
||||
availableBaseModels.value = baseModels
|
||||
availableTags.value = tags
|
||||
folderTree.value = folders
|
||||
}
|
||||
|
||||
// Refresh preview with current filters
|
||||
const refreshPreview = async () => {
|
||||
const result = await api.fetchLoras({
|
||||
baseModels: selectedBaseModels.value,
|
||||
tagsInclude: includeTags.value,
|
||||
tagsExclude: excludeTags.value,
|
||||
foldersInclude: includeFolders.value,
|
||||
foldersExclude: excludeFolders.value,
|
||||
noCreditRequired: noCreditRequired.value || undefined,
|
||||
allowSelling: allowSelling.value || undefined,
|
||||
pageSize: 6
|
||||
})
|
||||
|
||||
previewItems.value = result.items
|
||||
matchCount.value = result.total
|
||||
buildConfig()
|
||||
}
|
||||
|
||||
// Debounced filter change handler
|
||||
let filterTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const onFilterChange = () => {
|
||||
if (filterTimeout) clearTimeout(filterTimeout)
|
||||
filterTimeout = setTimeout(() => {
|
||||
refreshPreview()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// Watch all filter changes
|
||||
watch([
|
||||
selectedBaseModels,
|
||||
includeTags,
|
||||
excludeTags,
|
||||
includeFolders,
|
||||
excludeFolders,
|
||||
noCreditRequired,
|
||||
allowSelling
|
||||
], onFilterChange, { deep: true })
|
||||
|
||||
return {
|
||||
// Filter state
|
||||
selectedBaseModels,
|
||||
includeTags,
|
||||
excludeTags,
|
||||
includeFolders,
|
||||
excludeFolders,
|
||||
noCreditRequired,
|
||||
allowSelling,
|
||||
|
||||
// Available options
|
||||
availableBaseModels,
|
||||
availableTags,
|
||||
folderTree,
|
||||
|
||||
// Preview state
|
||||
previewItems,
|
||||
matchCount,
|
||||
isLoading,
|
||||
|
||||
// Actions
|
||||
buildConfig,
|
||||
restoreFromConfig,
|
||||
fetchFilterOptions,
|
||||
refreshPreview
|
||||
}
|
||||
}
|
||||
|
||||
export type LoraPoolStateReturn = ReturnType<typeof useLoraPoolState>
|
||||
31
vue-widgets/src/composables/useModalState.ts
Normal file
31
vue-widgets/src/composables/useModalState.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export type ModalType = 'baseModels' | 'includeTags' | 'excludeTags' | null
|
||||
|
||||
export function useModalState() {
|
||||
const activeModal = ref<ModalType>(null)
|
||||
|
||||
const isOpen = computed(() => activeModal.value !== null)
|
||||
|
||||
const openModal = (modal: ModalType) => {
|
||||
activeModal.value = modal
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
activeModal.value = null
|
||||
}
|
||||
|
||||
const isModalOpen = (modal: ModalType) => {
|
||||
return activeModal.value === modal
|
||||
}
|
||||
|
||||
return {
|
||||
activeModal,
|
||||
isOpen,
|
||||
openModal,
|
||||
closeModal,
|
||||
isModalOpen
|
||||
}
|
||||
}
|
||||
|
||||
export type ModalStateReturn = ReturnType<typeof useModalState>
|
||||
Reference in New Issue
Block a user