diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index d5004d5a8..68be19441 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -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
diff --git a/package-lock.json b/package-lock.json
index d0cd31bc6..19aa7766f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 7fa6ec47c..f1dbf0024 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/dialog/content/manager/ManagerDialogContent.vue b/src/components/dialog/content/manager/ManagerDialogContent.vue
index e44575700..21b0e68d7 100644
--- a/src/components/dialog/content/manager/ManagerDialogContent.vue
+++ b/src/components/dialog/content/manager/ManagerDialogContent.vue
@@ -30,9 +30,8 @@
([
])
const selectedTab = ref(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
-}
diff --git a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue
index b7ebd5d53..027d334ce 100644
--- a/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue
+++ b/src/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue
@@ -4,8 +4,7 @@
- {{ $t('g.resultsCount', { count: searchResults.length }) }}
+ {{ $t('g.resultsCount', { count: searchResults?.length || 0 }) }}
@@ -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('searchQuery')
+const searchMode = defineModel('searchMode', { default: 'packs' })
+const sortField = defineModel('sortField', { default: 'downloads' })
+
const { t } = useI18n()
-const currentSort = ref(DEFAULT_SORT)
-const currentFilter = ref(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[] = [
@@ -77,16 +65,7 @@ const sortOptions: SearchOption[] = [
{ id: 'category', label: t('g.category') }
]
const filterOptions: SearchOption[] = [
- { 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)
-}
diff --git a/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue b/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue
index 3032aaa09..392ed6b6b 100644
--- a/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue
+++ b/src/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue
@@ -2,12 +2,12 @@
{{ label }}:
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[]
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)
-}
diff --git a/src/composables/useRegistrySearch.ts b/src/composables/useRegistrySearch.ts
index 1b4d70c68..7c5750654 100644
--- a/src/composables/useRegistrySearch.ts
+++ b/src/composables/useRegistrySearch.ts
@@ -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('downloads')
+ const searchMode = ref<'nodes' | 'packs'>('packs')
const pageSize = ref(DEFAULT_PAGE_SIZE)
- const sortField = ref(DEFAULT_SORT_FIELD)
- const searchResults = ref([])
+ const pageNumber = ref(0)
+ const searchQuery = ref('')
+ const results = ref([])
- 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(() =>
+ 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
}
}
diff --git a/src/services/algoliaSearchService.ts b/src/services/algoliaSearchService.ts
new file mode 100644
index 000000000..e3268205d
--- /dev/null
+++ b/src/services/algoliaSearchService.ts
@@ -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] extends undefined | null ? undefined : NonNullable[K2]
+
+type RegistryNodePack = components['schemas']['Node']
+
+export interface AlgoliaNodePack {
+ objectID: RegistryNodePack['id']
+ name: RegistryNodePack['name']
+ publisher_id: SafeNestedProperty
+ 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[]> => {
+ const { pageSize, pageNumber } = params
+ const rest = omit(params, ['pageSize', 'pageNumber'])
+
+ const { results } = await searchClient.search({
+ requests: [
+ {
+ query,
+ indexName: 'nodes_index',
+ attributesToRetrieve: RETRIEVE_ATTRIBUTES,
+ ...rest,
+ hitsPerPage: pageSize,
+ length: pageSize,
+ page: pageNumber
+ }
+ ],
+ strategy: 'none'
+ })
+
+ // Narrow from `SearchResponse | SearchForFacetValuesResponse` to `SearchResponse`
+ return 'hits' in results[0] ? results[0].hits : [] // Only querying a single index for now
+ }
+
+ return {
+ searchPacks,
+ toRegistryPack
+ }
+}
diff --git a/src/types/algoliasearch-lite.d.ts b/src/types/algoliasearch-lite.d.ts
new file mode 100644
index 000000000..1514d272a
--- /dev/null
+++ b/src/types/algoliasearch-lite.d.ts
@@ -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
+}
diff --git a/vite.config.mts b/vite.config.mts
index 2cbb7280e..3678c70f4 100644
--- a/vite.config.mts
+++ b/vite.config.mts
@@ -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: {