Basic commandbox

This commit is contained in:
huchenlei
2025-08-09 21:22:51 -04:00
parent 109542dca3
commit a4d7b4dd55
7 changed files with 536 additions and 10 deletions

View File

@@ -0,0 +1,79 @@
<template>
<div class="comfy-command-search-item">
<span v-if="command.icon" class="item-icon" :class="command.icon" />
<span v-else class="item-icon pi pi-chevron-right" />
<span class="item-label">
<span v-html="highlightedLabel" />
</span>
<span v-if="command.keybinding" class="item-keybinding">
{{ command.keybinding.combo.toString() }}
</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { ComfyCommandImpl } from '@/stores/commandStore'
const { command, currentQuery } = defineProps<{
command: ComfyCommandImpl
currentQuery: string
}>()
const highlightedLabel = computed(() => {
const label = command.label || command.id
if (!currentQuery) return label
// Simple highlighting logic - case insensitive
const regex = new RegExp(
`(${currentQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`,
'gi'
)
return label.replace(regex, '<mark>$1</mark>')
})
</script>
<style scoped>
.comfy-command-search-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
width: 100%;
}
.item-icon {
flex-shrink: 0;
width: 1.25rem;
text-align: center;
color: var(--p-text-muted-color);
}
.item-label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-label :deep(mark) {
background-color: var(--p-highlight-background);
color: var(--p-highlight-color);
font-weight: 600;
padding: 0;
}
.item-keybinding {
flex-shrink: 0;
font-size: 0.75rem;
padding: 0.125rem 0.375rem;
border: 1px solid var(--p-content-border-color);
border-radius: 0.25rem;
background: var(--p-content-hover-background);
color: var(--p-text-muted-color);
font-family: monospace;
}
</style>

View File

@@ -3,7 +3,7 @@
class="comfy-vue-node-search-container flex justify-center items-center w-full min-w-96"
>
<div
v-if="enableNodePreview"
v-if="enableNodePreview && !isCommandMode"
class="comfy-vue-node-preview-container absolute left-[-350px] top-[50px]"
>
<NodePreview
@@ -14,6 +14,7 @@
</div>
<Button
v-if="!isCommandMode"
icon="pi pi-filter"
severity="secondary"
class="filter-button z-10"
@@ -49,13 +50,22 @@
auto-option-focus
force-selection
multiple
:option-label="'display_name'"
:option-label="getOptionLabel"
@complete="search($event.query)"
@option-select="emit('addNode', $event.value)"
@option-select="onOptionSelect($event.value)"
@focused-option-changed="setHoverSuggestion($event)"
>
<template #option="{ option }">
<NodeSearchItem :node-def="option" :current-query="currentQuery" />
<CommandSearchItem
v-if="isCommandMode"
:command="option"
:current-query="currentQuery"
/>
<NodeSearchItem
v-else
:node-def="option"
:current-query="currentQuery"
/>
</template>
<!-- FilterAndValue -->
<template #chip="{ value }">
@@ -80,13 +90,16 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import NodePreview from '@/components/node/NodePreview.vue'
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
import CommandSearchItem from '@/components/searchbox/CommandSearchItem.vue'
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
import { CommandSearchService } from '@/services/commandSearchService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
import {
ComfyNodeDefImpl,
useNodeDefStore,
@@ -99,6 +112,7 @@ import SearchFilterChip from '../common/SearchFilterChip.vue'
const settingStore = useSettingStore()
const { t } = useI18n()
const commandStore = useCommandStore()
const enableNodePreview = computed(() =>
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
@@ -111,18 +125,50 @@ const { filters, searchLimit = 64 } = defineProps<{
const nodeSearchFilterVisible = ref(false)
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
const suggestions = ref<ComfyNodeDefImpl[]>([])
const suggestions = ref<ComfyNodeDefImpl[] | ComfyCommandImpl[]>([])
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
const currentQuery = ref('')
const isCommandMode = ref(false)
// Initialize command search service
const commandSearchService = ref<CommandSearchService | null>(null)
const placeholder = computed(() => {
if (isCommandMode.value) {
return t('g.searchCommands', 'Search commands') + '...'
}
return filters.length === 0 ? t('g.searchNodes') + '...' : ''
})
const nodeDefStore = useNodeDefStore()
const nodeFrequencyStore = useNodeFrequencyStore()
// Initialize command search service with commands
watch(
() => commandStore.commands,
(commands) => {
commandSearchService.value = new CommandSearchService(commands)
},
{ immediate: true }
)
const search = (query: string) => {
const queryIsEmpty = query === '' && filters.length === 0
currentQuery.value = query
// Check if we're in command mode (query starts with ">")
if (query.startsWith('>')) {
isCommandMode.value = true
if (commandSearchService.value) {
suggestions.value = commandSearchService.value.searchCommands(query, {
limit: searchLimit
})
}
return
}
// Normal node search mode
isCommandMode.value = false
const queryIsEmpty = query === '' && filters.length === 0
suggestions.value = queryIsEmpty
? nodeFrequencyStore.topNodeDefs
: [
@@ -132,7 +178,18 @@ const search = (query: string) => {
]
}
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
const emit = defineEmits<{
(
e: 'addFilter',
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
): void
(
e: 'removeFilter',
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
): void
(e: 'addNode', nodeDef: ComfyNodeDefImpl): void
(e: 'executeCommand', command: ComfyCommandImpl): void
}>()
let inputElement: HTMLInputElement | null = null
const reFocusInput = async () => {
@@ -160,11 +217,28 @@ const onRemoveFilter = async (
await reFocusInput()
}
const setHoverSuggestion = (index: number) => {
if (index === -1) {
if (index === -1 || isCommandMode.value) {
hoveredSuggestion.value = null
return
}
const value = suggestions.value[index]
const value = suggestions.value[index] as ComfyNodeDefImpl
hoveredSuggestion.value = value
}
const onOptionSelect = (option: ComfyNodeDefImpl | ComfyCommandImpl) => {
if (isCommandMode.value) {
emit('executeCommand', option as ComfyCommandImpl)
} else {
emit('addNode', option as ComfyNodeDefImpl)
}
}
const getOptionLabel = (
option: ComfyNodeDefImpl | ComfyCommandImpl
): string => {
if ('display_name' in option) {
return option.display_name
}
return option.label || option.id
}
</script>

View File

@@ -26,6 +26,7 @@
@add-filter="addFilter"
@remove-filter="removeFilter"
@add-node="addNode"
@execute-command="executeCommand"
/>
</template>
</Dialog>
@@ -46,6 +47,7 @@ import {
} from '@/lib/litegraph/src/litegraph'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import { useLitegraphService } from '@/services/litegraphService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -62,6 +64,7 @@ let disconnectOnReset = false
const settingStore = useSettingStore()
const litegraphService = useLitegraphService()
const commandStore = useCommandStore()
const { visible } = storeToRefs(useSearchBoxStore())
const dismissable = ref(true)
@@ -109,6 +112,14 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
window.requestAnimationFrame(closeDialog)
}
const executeCommand = async (command: ComfyCommandImpl) => {
// Close the dialog immediately
closeDialog()
// Execute the command
await commandStore.execute(command.id)
}
const newSearchBoxEnabled = computed(
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
)