Files
ComfyUI-Lora-Manager/vue-widgets/src/composables/useAutocomplete.ts
Will Miao d5a2bd1e24 feat: add custom words autocomplete support for Prompt node
Adds custom words autocomplete functionality similar to comfyui-custom-scripts,
with the following features:

Backend (Python):
- Create CustomWordsService for CSV parsing and priority-based search
- Add API endpoints: GET/POST /api/lm/custom-words and
  GET /api/lm/custom-words/search
- Share storage with pysssss plugin (checks for their user/autocomplete.txt first)
- Fallback to Lora Manager's user directory for storage

Frontend (JavaScript/Vue):
- Add 'custom_words' and 'prompt' model types to autocomplete system
- Prompt node now supports dual-mode autocomplete:
  * Type 'emb:' prefix → search embeddings
  * Type normally → search custom words (no prefix required)
- Add AUTOCOMPLETE_TEXT_PROMPT widget type
- Update Vue component and composable types

Key Features:
- CSV format: word[,priority] compatible with danbooru-tags.txt
- Priority-based sorting: 20% top priority + prefix + include matches
- Preview tooltip for embeddings (not for custom words)
- Dynamic endpoint switching based on prefix detection

Breaking Changes:
- Prompt (LoraManager) node widget type changed from
  AUTOCOMPLETE_TEXT_EMBEDDINGS to AUTOCOMPLETE_TEXT_PROMPT
- Removed standalone web/comfyui/prompt.js (integrated into main widgets)

Fixes comfy_dir path calculation by prioritizing folder_paths.base_path
from ComfyUI when available, with fallback to computed path.
2026-01-25 12:24:32 +08:00

111 lines
2.8 KiB
TypeScript

import { ref, onMounted, onUnmounted, type Ref } from 'vue'
// Dynamic import type for AutoComplete class
type AutoCompleteClass = new (
inputElement: HTMLTextAreaElement,
modelType: 'loras' | 'embeddings' | 'custom_words' | 'prompt',
options?: AutocompleteOptions
) => AutoCompleteInstance
interface AutocompleteOptions {
maxItems?: number
minChars?: number
debounceDelay?: number
showPreview?: boolean
}
interface AutoCompleteInstance {
destroy: () => void
isValid: () => boolean
refreshCaretHelper: () => void
}
export interface UseAutocompleteOptions {
showPreview?: boolean
maxItems?: number
minChars?: number
debounceDelay?: number
}
export function useAutocomplete(
textareaRef: Ref<HTMLTextAreaElement | null>,
modelType: 'loras' | 'embeddings' | 'custom_words' | 'prompt' = 'loras',
options: UseAutocompleteOptions = {}
) {
const autocompleteInstance = ref<AutoCompleteInstance | null>(null)
const isInitialized = ref(false)
const defaultOptions: AutocompleteOptions = {
maxItems: 20,
minChars: 1,
debounceDelay: 200,
showPreview: true,
...options
}
const initAutocomplete = async () => {
if (!textareaRef.value) {
console.warn('[useAutocomplete] Textarea ref is null, cannot initialize')
return
}
if (autocompleteInstance.value) {
console.log('[useAutocomplete] Already initialized, skipping')
return
}
try {
// Dynamically import the AutoComplete class
const module = await import(/* @vite-ignore */ `${'../autocomplete.js'}`)
const AutoComplete: AutoCompleteClass = module.AutoComplete
autocompleteInstance.value = new AutoComplete(
textareaRef.value,
modelType,
defaultOptions
)
isInitialized.value = true
console.log(`[useAutocomplete] Initialized for ${modelType}`)
} catch (error) {
console.error('[useAutocomplete] Failed to initialize:', error)
}
}
const destroyAutocomplete = () => {
if (autocompleteInstance.value) {
autocompleteInstance.value.destroy()
autocompleteInstance.value = null
isInitialized.value = false
console.log('[useAutocomplete] Destroyed')
}
}
const refreshCaretHelper = () => {
if (autocompleteInstance.value) {
autocompleteInstance.value.refreshCaretHelper()
}
}
onMounted(() => {
// Initialize autocomplete after component is mounted
// Use nextTick-like delay to ensure DOM is fully ready
setTimeout(() => {
initAutocomplete()
}, 0)
})
onUnmounted(() => {
destroyAutocomplete()
})
return {
autocompleteInstance,
isInitialized,
initAutocomplete,
destroyAutocomplete,
refreshCaretHelper
}
}
export type UseAutocompleteReturn = ReturnType<typeof useAutocomplete>