mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
[Manager] Add Algolia search (#3036)
This commit is contained in:
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -27,6 +27,8 @@ jobs:
|
||||
- name: Build project
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run fetch-templates
|
||||
|
||||
191
package-lock.json
generated
191
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"algoliasearch": "^5.21.0",
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
@@ -113,6 +114,173 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-abtesting": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.21.0.tgz",
|
||||
"integrity": "sha512-I239aSmXa3pXDhp3AWGaIfesqJBNFA7drUM8SIfNxMIzvQXUnHRf4rW1o77QXLI/nIClNsb8KOLaB62gO9LnlQ==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-analytics": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.21.0.tgz",
|
||||
"integrity": "sha512-OxoUfeG9G4VE4gS7B4q65KkHzdGsQsDwxQfR5J9uKB8poSGuNlHJWsF3ABqCkc5VliAR0m8KMjsQ9o/kOpEGnQ==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-common": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.21.0.tgz",
|
||||
"integrity": "sha512-iHLgDQFyZNe9M16vipbx6FGOA8NoMswHrfom/QlCGoyh7ntjGvfMb+J2Ss8rRsAlOWluv8h923Ku3QVaB0oWDQ==",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-insights": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.21.0.tgz",
|
||||
"integrity": "sha512-y7XBO9Iwb75FLDl95AYcWSLIViJTpR5SUUCyKsYhpP9DgyUqWbISqDLXc96TS9shj+H+7VsTKA9cJK8NUfVN6g==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-personalization": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.21.0.tgz",
|
||||
"integrity": "sha512-6KU658lD9Tss4oCX6c/O15tNZxw7vR+WAUG95YtZzYG/KGJHTpy2uckqbMmC2cEK4a86FAq4pH5azSJ7cGMjuw==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-query-suggestions": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.21.0.tgz",
|
||||
"integrity": "sha512-pG6MyVh1v0X+uwrKHn3U+suHdgJ2C+gug+UGkNHfMELHMsEoWIAQhxMBOFg7hCnWBFjQnuq6qhM3X9X5QO3d9Q==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-search": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.21.0.tgz",
|
||||
"integrity": "sha512-nZfgJH4njBK98tFCmCW1VX/ExH4bNOl9DSboxeXGgvhoL0fG1+4DDr/mrLe21OggVCQqHwXBMh6fFInvBeyhiQ==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/ingestion": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.21.0.tgz",
|
||||
"integrity": "sha512-k6MZxLbZphGN5uRri9J/krQQBjUrqNcScPh985XXEFXbSCRvOPKVtjjLdVjGVHXXPOQgKrIZHxIdRNbHS+wVuA==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/monitoring": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.21.0.tgz",
|
||||
"integrity": "sha512-FiW5nnmyHvaGdorqLClw3PM6keXexAMiwbwJ9xzQr4LcNefLG3ln82NafRPgJO/z0dETAOKjds5aSmEFMiITHQ==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/recommend": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.21.0.tgz",
|
||||
"integrity": "sha512-+JXavbbliaLmah5QNgc/TDW/+r0ALa+rGhg5Y7+pF6GpNnzO0L+nlUaDNE8QbiJfz54F9BkwFUnJJeRJAuzTFw==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/requester-browser-xhr": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.21.0.tgz",
|
||||
"integrity": "sha512-Iw+Yj5hOmo/iixHS94vEAQ3zi5GPpJywhfxn1el/zWo4AvPIte/+1h9Ywgw/+3M7YBj4jgAkScxjxQCxzLBsjA==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/requester-fetch": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.21.0.tgz",
|
||||
"integrity": "sha512-Z00SRLlIFj3SjYVfsd9Yd3kB3dUwQFAkQG18NunWP7cix2ezXpJqA+xAoEf9vc4QZHdxU3Gm8gHAtRiM2iVaTQ==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/requester-node-http": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.21.0.tgz",
|
||||
"integrity": "sha512-WqU0VumUILrIeVYCTGZlyyZoC/tbvhiyPxfGRRO1cSjxN558bnJLlR2BvS0SJ5b75dRNK7HDvtXo2QoP9eLfiA==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@@ -3859,6 +4027,29 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/algoliasearch": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.21.0.tgz",
|
||||
"integrity": "sha512-hexLq2lSO1K5SW9j21Ubc+q9Ptx7dyRTY7se19U8lhIlVMLCNXWCyQ6C22p9ez8ccX0v7QVmwkl2l1CnuGoO2Q==",
|
||||
"dependencies": {
|
||||
"@algolia/client-abtesting": "5.21.0",
|
||||
"@algolia/client-analytics": "5.21.0",
|
||||
"@algolia/client-common": "5.21.0",
|
||||
"@algolia/client-insights": "5.21.0",
|
||||
"@algolia/client-personalization": "5.21.0",
|
||||
"@algolia/client-query-suggestions": "5.21.0",
|
||||
"@algolia/client-search": "5.21.0",
|
||||
"@algolia/ingestion": "1.21.0",
|
||||
"@algolia/monitoring": "1.21.0",
|
||||
"@algolia/recommend": "5.21.0",
|
||||
"@algolia/requester-browser-xhr": "5.21.0",
|
||||
"@algolia/requester-fetch": "5.21.0",
|
||||
"@algolia/requester-node-http": "5.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz",
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-serialize": "^0.13.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"algoliasearch": "^5.21.0",
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
|
||||
@@ -30,9 +30,8 @@
|
||||
<RegistrySearchBar
|
||||
v-if="!hideSearchBar"
|
||||
v-model:searchQuery="searchQuery"
|
||||
v-model:searchMode="searchMode"
|
||||
:searchResults="searchResults"
|
||||
@update:sortBy="handleSortChange"
|
||||
@update:filterBy="handleFilterChange"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
@@ -42,14 +41,14 @@
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
<NoResultsPlaceholder
|
||||
v-else-if="error || searchResults.length === 0"
|
||||
v-else-if="searchResults.length === 0"
|
||||
:title="
|
||||
error
|
||||
comfyManagerStore.error
|
||||
? $t('manager.errorConnecting')
|
||||
: $t('manager.noResultsFound')
|
||||
"
|
||||
:message="
|
||||
error
|
||||
comfyManagerStore.error
|
||||
? $t('manager.tryAgainLater')
|
||||
: $t('manager.tryDifferentSearch')
|
||||
"
|
||||
@@ -121,7 +120,7 @@ import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollap
|
||||
import { useInstalledPacks } from '@/composables/useInstalledPacks'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { PackField, TabItem } from '@/types/comfyManagerTypes'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_CARD_SIZE = 512
|
||||
@@ -142,9 +141,9 @@ const tabs = ref<TabItem[]>([
|
||||
])
|
||||
const selectedTab = ref<TabItem>(tabs.value[0])
|
||||
|
||||
const { searchQuery, pageNumber, sortField, isLoading, error, searchResults } =
|
||||
const { searchQuery, pageNumber, isLoading, searchResults, searchMode } =
|
||||
useRegistrySearch()
|
||||
pageNumber.value = 1
|
||||
pageNumber.value = 0
|
||||
|
||||
const isInitialLoad = computed(
|
||||
() => searchResults.value.length === 0 && searchQuery.value === ''
|
||||
@@ -216,12 +215,4 @@ const handleGridContainerClick = (event: MouseEvent) => {
|
||||
|
||||
const showInfoPanel = computed(() => selectedNodePacks.value.length > 0)
|
||||
const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1)
|
||||
|
||||
const currentFilterBy = ref('all')
|
||||
const handleSortChange = (sortBy: PackField) => {
|
||||
sortField.value = sortBy
|
||||
}
|
||||
const handleFilterChange = (filterBy: PackField) => {
|
||||
currentFilterBy.value = filterBy
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<IconField class="w-5/12">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="$emit('update:searchQuery', $event)"
|
||||
v-model="searchQuery"
|
||||
:placeholder="$t('manager.searchPlaceholder')"
|
||||
class="w-full rounded-2xl"
|
||||
autofocus
|
||||
@@ -15,21 +14,19 @@
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
<SearchFilterDropdown
|
||||
v-model="currentFilter"
|
||||
v-model:modelValue="searchMode"
|
||||
:options="filterOptions"
|
||||
:label="$t('g.filter')"
|
||||
@update:model-value="handleFilterChange"
|
||||
/>
|
||||
<SearchFilterDropdown
|
||||
v-model="currentSort"
|
||||
v-model:modelValue="sortField"
|
||||
:options="sortOptions"
|
||||
:label="$t('g.sort')"
|
||||
@update:model-value="handleSortChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 ml-6">
|
||||
<small v-if="hasResults" class="text-color-secondary">
|
||||
{{ $t('g.resultsCount', { count: searchResults.length }) }}
|
||||
{{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,34 +37,25 @@
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import type { PackField, SearchOption } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_SORT: PackField = 'downloads'
|
||||
const DEFAULT_FILTER = 'nodePack'
|
||||
|
||||
const props = defineProps<{
|
||||
searchQuery: string
|
||||
searchResults?: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
const searchMode = defineModel<string>('searchMode', { default: 'packs' })
|
||||
const sortField = defineModel<PackField>('sortField', { default: 'downloads' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const currentSort = ref<PackField>(DEFAULT_SORT)
|
||||
const currentFilter = ref<string>(DEFAULT_FILTER)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:searchQuery': [value: string]
|
||||
'update:sortBy': [value: PackField]
|
||||
'update:filterBy': [value: string]
|
||||
}>()
|
||||
|
||||
const hasResults = computed(
|
||||
() => props.searchQuery.trim() && props.searchResults?.length
|
||||
() => searchQuery.value?.trim() && props.searchResults?.length
|
||||
)
|
||||
|
||||
const sortOptions: SearchOption<PackField>[] = [
|
||||
@@ -77,16 +65,7 @@ const sortOptions: SearchOption<PackField>[] = [
|
||||
{ id: 'category', label: t('g.category') }
|
||||
]
|
||||
const filterOptions: SearchOption<string>[] = [
|
||||
{ id: 'nodePack', label: t('manager.filter.nodePack') },
|
||||
{ id: 'node', label: t('g.nodes') }
|
||||
{ id: 'packs', label: t('manager.filter.nodePack') },
|
||||
{ id: 'nodes', label: t('g.nodes') }
|
||||
]
|
||||
|
||||
const handleSortChange = () => {
|
||||
// TODO: emit to Algolia service
|
||||
emit('update:sortBy', currentSort.value)
|
||||
}
|
||||
const handleFilterChange = () => {
|
||||
// TODO: emit to Algolia service
|
||||
emit('update:filterBy', currentFilter.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-muted">{{ label }}:</span>
|
||||
<Dropdown
|
||||
v-model="selectedValue"
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="$emit('update:modelValue', $event)"
|
||||
:options="options"
|
||||
optionLabel="label"
|
||||
optionValue="id"
|
||||
class="min-w-[6rem] border-none bg-transparent shadow-none"
|
||||
@change="handleChange"
|
||||
:pt="{
|
||||
input: { class: 'py-0 px-1 border-none' },
|
||||
trigger: { class: 'hidden' },
|
||||
@@ -20,26 +20,16 @@
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SearchOption } from '@/types/comfyManagerTypes'
|
||||
|
||||
const { modelValue, options, label } = defineProps<{
|
||||
modelValue: T
|
||||
defineProps<{
|
||||
options: SearchOption<T>[]
|
||||
label: string
|
||||
modelValue: T
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: T]
|
||||
}>()
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const handleChange = () => {
|
||||
emit('update:modelValue', selectedValue.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,72 +1,77 @@
|
||||
import { debounce } from 'lodash'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { memoize } from 'lodash'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import {
|
||||
AlgoliaNodePack,
|
||||
SearchAttribute,
|
||||
useAlgoliaSearchService
|
||||
} from '@/services/algoliaSearchService'
|
||||
import { PackField } from '@/types/comfyManagerTypes'
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 256
|
||||
const DEFAULT_PAGE_SIZE = 60
|
||||
const DEFAULT_SORT_FIELD: keyof components['schemas']['Node'] = 'downloads'
|
||||
const SEARCH_DEBOUNCE_TIME = 16
|
||||
const DEFAULT_PAGE_SIZE = 64
|
||||
|
||||
/**
|
||||
* Composable for managing UI state of Comfy Node Registry search.
|
||||
*/
|
||||
export function useRegistrySearch() {
|
||||
const registryStore = useComfyRegistryStore()
|
||||
const registryService = useComfyRegistryService()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const pageNumber = ref(1)
|
||||
const isLoading = ref(false)
|
||||
const sortField = ref<PackField>('downloads')
|
||||
const searchMode = ref<'nodes' | 'packs'>('packs')
|
||||
const pageSize = ref(DEFAULT_PAGE_SIZE)
|
||||
const sortField = ref<keyof components['schemas']['Node']>(DEFAULT_SORT_FIELD)
|
||||
const searchResults = ref<components['schemas']['Node'][]>([])
|
||||
const pageNumber = ref(0)
|
||||
const searchQuery = ref('')
|
||||
const results = ref<AlgoliaNodePack[]>([])
|
||||
|
||||
const search = async () => {
|
||||
try {
|
||||
const isEmptySearch = searchQuery.value === ''
|
||||
const result = isEmptySearch
|
||||
? await registryStore.listAllPacks({
|
||||
page: pageNumber.value,
|
||||
limit: pageSize.value,
|
||||
sort: [sortField.value]
|
||||
})
|
||||
: await registryService.search({
|
||||
search: searchQuery.value,
|
||||
page: pageNumber.value,
|
||||
limit: pageSize.value
|
||||
})
|
||||
const searchAttributes = computed<SearchAttribute[]>(() =>
|
||||
searchMode.value === 'nodes' ? ['comfy_nodes'] : ['name', 'description']
|
||||
)
|
||||
|
||||
if (result) {
|
||||
searchResults.value = result.nodes || []
|
||||
} else {
|
||||
searchResults.value = []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading packs:', err)
|
||||
searchResults.value = []
|
||||
}
|
||||
const resultsAsRegistryPacks = computed(() =>
|
||||
results.value ? results.value.map(algoliaToRegistry) : []
|
||||
)
|
||||
const resultsAsNodes = computed(() =>
|
||||
results.value
|
||||
? results.value.reduce(
|
||||
(acc, hit) => acc.concat(hit.comfy_nodes),
|
||||
[] as string[]
|
||||
)
|
||||
: []
|
||||
)
|
||||
|
||||
const { searchPacks, toRegistryPack } = useAlgoliaSearchService()
|
||||
|
||||
const algoliaToRegistry = memoize(
|
||||
toRegistryPack,
|
||||
(algoliaNode: AlgoliaNodePack) => algoliaNode.id
|
||||
)
|
||||
|
||||
const onQueryChange = async () => {
|
||||
isLoading.value = true
|
||||
results.value = await searchPacks(searchQuery.value, {
|
||||
pageSize: pageSize.value,
|
||||
pageNumber: pageNumber.value,
|
||||
restrictSearchableAttributes: searchAttributes.value
|
||||
})
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const debouncedSearch = debounce(search, SEARCH_DEBOUNCE_TIME)
|
||||
|
||||
// Debounce search when query changes
|
||||
watch(() => searchQuery.value, debouncedSearch)
|
||||
|
||||
watch(() => [pageNumber.value, sortField.value], search, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
debouncedSearch.cancel() // Cancel debounced searches
|
||||
registryStore.cancelRequests() // Cancel in-flight requests
|
||||
watch([pageNumber, sortField, searchMode], onQueryChange, {
|
||||
immediate: true
|
||||
})
|
||||
watchDebounced(searchQuery, onQueryChange, {
|
||||
debounce: SEARCH_DEBOUNCE_TIME
|
||||
})
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
sortField,
|
||||
searchMode,
|
||||
searchQuery,
|
||||
searchResults,
|
||||
isLoading: registryService.isLoading,
|
||||
error: registryService.error
|
||||
searchResults: resultsAsRegistryPacks,
|
||||
nodeSearchResults: resultsAsNodes
|
||||
}
|
||||
}
|
||||
|
||||
153
src/services/algoliaSearchService.ts
Normal file
153
src/services/algoliaSearchService.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type {
|
||||
BaseSearchParamsWithoutQuery,
|
||||
Hit
|
||||
} from 'algoliasearch/dist/lite/browser'
|
||||
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
|
||||
type SafeNestedProperty<
|
||||
T,
|
||||
K1 extends keyof T,
|
||||
K2 extends keyof NonNullable<T[K1]>
|
||||
> = T[K1] extends undefined | null ? undefined : NonNullable<T[K1]>[K2]
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
|
||||
export interface AlgoliaNodePack {
|
||||
objectID: RegistryNodePack['id']
|
||||
name: RegistryNodePack['name']
|
||||
publisher_id: SafeNestedProperty<RegistryNodePack, 'publisher', 'id'>
|
||||
description: RegistryNodePack['description']
|
||||
comfy_nodes: string[]
|
||||
total_install: RegistryNodePack['downloads']
|
||||
id: RegistryNodePack['id']
|
||||
create_time: string
|
||||
update_time: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'createdAt'
|
||||
>
|
||||
license: RegistryNodePack['license']
|
||||
repository_url: RegistryNodePack['repository']
|
||||
status: RegistryNodePack['status']
|
||||
latest_version: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'version'
|
||||
>
|
||||
latest_version_status: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'status'
|
||||
>
|
||||
comfy_node_extract_status: SafeNestedProperty<
|
||||
RegistryNodePack,
|
||||
'latest_version',
|
||||
'comfy_node_extract_status'
|
||||
>
|
||||
}
|
||||
|
||||
export type SearchAttribute = keyof AlgoliaNodePack
|
||||
|
||||
const RETRIEVE_ATTRIBUTES: SearchAttribute[] = [
|
||||
'comfy_nodes',
|
||||
'name',
|
||||
'description',
|
||||
'latest_version',
|
||||
'status',
|
||||
'publisher_id',
|
||||
'total_install',
|
||||
'update_time',
|
||||
'license',
|
||||
'repository_url',
|
||||
'latest_version_status',
|
||||
'comfy_node_extract_status',
|
||||
'id'
|
||||
]
|
||||
|
||||
type SearchNodePacksParams = BaseSearchParamsWithoutQuery & {
|
||||
pageSize: number
|
||||
pageNumber: number
|
||||
restrictSearchableAttributes: SearchAttribute[]
|
||||
}
|
||||
|
||||
export const useAlgoliaSearchService = () => {
|
||||
const searchClient = algoliasearch(__ALGOLIA_APP_ID__, __ALGOLIA_API_KEY__)
|
||||
|
||||
const toRegistryLatestVersion = (
|
||||
algoliaNode: AlgoliaNodePack
|
||||
): RegistryNodePack['latest_version'] => {
|
||||
return {
|
||||
version: algoliaNode.latest_version,
|
||||
createdAt: algoliaNode.update_time,
|
||||
status: algoliaNode.latest_version_status,
|
||||
comfy_node_extract_status:
|
||||
algoliaNode.comfy_node_extract_status ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
const toRegistryPublisher = (
|
||||
algoliaNode: AlgoliaNodePack
|
||||
): RegistryNodePack['publisher'] => {
|
||||
return {
|
||||
id: algoliaNode.publisher_id,
|
||||
name: algoliaNode.publisher_id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from node pack in Algolia format to Comfy Registry format
|
||||
*/
|
||||
function toRegistryPack(algoliaNode: AlgoliaNodePack): RegistryNodePack {
|
||||
return {
|
||||
id: algoliaNode.id ?? algoliaNode.objectID,
|
||||
name: algoliaNode.name,
|
||||
description: algoliaNode.description,
|
||||
repository: algoliaNode.repository_url,
|
||||
license: algoliaNode.license,
|
||||
downloads: algoliaNode.total_install,
|
||||
status: algoliaNode.status,
|
||||
latest_version: toRegistryLatestVersion(algoliaNode),
|
||||
publisher: toRegistryPublisher(algoliaNode)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for node packs in Algolia
|
||||
*/
|
||||
const searchPacks = async (
|
||||
query: string,
|
||||
params: SearchNodePacksParams
|
||||
): Promise<Hit<AlgoliaNodePack>[]> => {
|
||||
const { pageSize, pageNumber } = params
|
||||
const rest = omit(params, ['pageSize', 'pageNumber'])
|
||||
|
||||
const { results } = await searchClient.search<AlgoliaNodePack>({
|
||||
requests: [
|
||||
{
|
||||
query,
|
||||
indexName: 'nodes_index',
|
||||
attributesToRetrieve: RETRIEVE_ATTRIBUTES,
|
||||
...rest,
|
||||
hitsPerPage: pageSize,
|
||||
length: pageSize,
|
||||
page: pageNumber
|
||||
}
|
||||
],
|
||||
strategy: 'none'
|
||||
})
|
||||
|
||||
// Narrow from `SearchResponse<T> | SearchForFacetValuesResponse` to `SearchResponse<T>`
|
||||
return 'hits' in results[0] ? results[0].hits : [] // Only querying a single index for now
|
||||
}
|
||||
|
||||
return {
|
||||
searchPacks,
|
||||
toRegistryPack
|
||||
}
|
||||
}
|
||||
19
src/types/algoliasearch-lite.d.ts
vendored
Normal file
19
src/types/algoliasearch-lite.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
declare module 'algoliasearch/dist/lite/builds/browser' {
|
||||
import { LiteClient, ClientOptions } from 'algoliasearch/dist/lite/browser'
|
||||
|
||||
/**
|
||||
* Creates a new Algolia Search client that uses the Lite API Client (Browser version)
|
||||
*
|
||||
* @param appId - Your Algolia Application ID
|
||||
* @param apiKey - Your Algolia API Key
|
||||
* @param options - Options for the client
|
||||
* @returns An Algolia Search client instance
|
||||
*/
|
||||
export function liteClient(
|
||||
appId: string,
|
||||
apiKey: string,
|
||||
options?: ClientOptions
|
||||
): LiteClient
|
||||
|
||||
export const apiClientVersion: string
|
||||
}
|
||||
@@ -182,7 +182,9 @@ export default defineConfig({
|
||||
__SENTRY_ENABLED__: JSON.stringify(
|
||||
!(process.env.NODE_ENV === 'development' || !process.env.SENTRY_DSN)
|
||||
),
|
||||
__SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || '')
|
||||
__SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || ''),
|
||||
__ALGOLIA_APP_ID__: JSON.stringify(process.env.VITE_ALGOLIA_APP_ID || ''),
|
||||
__ALGOLIA_API_KEY__: JSON.stringify(process.env.VITE_ALGOLIA_API_KEY || '')
|
||||
},
|
||||
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user