mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-31 09:45:46 +00:00
Compare commits
2 Commits
test2
...
cb/search-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3fe311a72 | ||
|
|
72b5444d5a |
@@ -13,14 +13,37 @@
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"src/types/vue-shim.d.ts"
|
||||
],
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"import",
|
||||
"oxc",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"vitest",
|
||||
"vue"
|
||||
],
|
||||
"rules": {
|
||||
"no-async-promise-executor": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "off",
|
||||
"no-redeclare": "error",
|
||||
"no-self-assign": "allow",
|
||||
"no-unused-expressions": "off",
|
||||
"no-unused-private-class-members": "off",
|
||||
"no-useless-rename": "off",
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/consistent-type-specifier-style": [
|
||||
"error",
|
||||
"prefer-top-level"
|
||||
],
|
||||
"jest/expect-expect": "off",
|
||||
"jest/no-conditional-expect": "off",
|
||||
"jest/no-disabled-tests": "off",
|
||||
"jest/no-standalone-expect": "off",
|
||||
"jest/valid-title": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
@@ -39,6 +62,7 @@
|
||||
"typescript/restrict-template-expressions": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"vue/no-import-compiler-macros": "error"
|
||||
"vue/no-import-compiler-macros": "error",
|
||||
"vue/no-dupe-keys": "error"
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@
|
||||
"declaration-property-value-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"typesSyntax": {
|
||||
"radial-gradient()": "| <any-value>"
|
||||
},
|
||||
"ignoreProperties": {
|
||||
"speak": ["none"],
|
||||
"app-region": ["drag", "no-drag"],
|
||||
@@ -56,10 +59,7 @@
|
||||
"function-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreFunctions": [
|
||||
"theme",
|
||||
"v-bind"
|
||||
]
|
||||
"ignoreFunctions": ["theme", "v-bind"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -135,7 +135,6 @@ export default defineConfig([
|
||||
allowInterfaces: 'always'
|
||||
}
|
||||
],
|
||||
'import-x/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
'import-x/no-useless-path-segments': 'error',
|
||||
'import-x/no-relative-packages': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
@@ -146,14 +145,13 @@ export default defineConfig([
|
||||
'vue/multi-word-component-names': 'off', // TODO: fix
|
||||
'vue/no-template-shadow': 'off', // TODO: fix
|
||||
'vue/match-component-import-name': 'error',
|
||||
/* Toggle on to do additional until we can clean up existing violations.
|
||||
'vue/no-unused-emit-declarations': 'error',
|
||||
'vue/no-unused-properties': 'error',
|
||||
'vue/no-unused-refs': 'error',
|
||||
'vue/no-use-v-else-with-v-for': 'error',
|
||||
'vue/no-useless-mustaches': 'error',
|
||||
'vue/no-useless-v-bind': 'error',
|
||||
// */
|
||||
'vue/one-component-per-file': 'off', // TODO: fix
|
||||
'vue/no-unused-emit-declarations': 'error',
|
||||
'vue/no-use-v-else-with-v-for': 'error',
|
||||
'vue/one-component-per-file': 'error',
|
||||
'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile
|
||||
// Restrict deprecated PrimeVue components
|
||||
'no-restricted-imports': [
|
||||
@@ -297,5 +295,14 @@ export default defineConfig([
|
||||
// Turn off ESLint rules that are already handled by oxlint
|
||||
...oxlint.buildFromOxlintConfigFile(
|
||||
path.resolve(import.meta.dirname, '.oxlintrc.json')
|
||||
)
|
||||
),
|
||||
{
|
||||
rules: {
|
||||
'import-x/default': 'off',
|
||||
'import-x/export': 'off',
|
||||
'import-x/namespace': 'off',
|
||||
'import-x/no-duplicates': 'off',
|
||||
'import-x/consistent-type-specifier-style': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
1648
pnpm-lock.yaml
generated
1648
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ packages:
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@comfyorg/comfyui-electron-types': 0.5.5
|
||||
'@eslint/js': ^9.35.0
|
||||
'@eslint/js': ^9.39.1
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
'@iconify/json': ^2.2.380
|
||||
'@iconify/tailwind': ^1.1.3
|
||||
@@ -17,7 +17,7 @@ catalog:
|
||||
'@nx/vite': 21.4.1
|
||||
'@pinia/testing': ^0.1.5
|
||||
'@playwright/test': ^1.52.0
|
||||
'@prettier/plugin-oxc': ^0.0.4
|
||||
'@prettier/plugin-oxc': ^0.1.3
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
@@ -48,15 +48,15 @@ catalog:
|
||||
axios: ^1.8.2
|
||||
cross-env: ^10.1.0
|
||||
dotenv: ^16.4.5
|
||||
eslint: ^9.34.0
|
||||
eslint: ^9.39.1
|
||||
eslint-config-prettier: ^10.1.8
|
||||
eslint-import-resolver-typescript: ^4.4.4
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-oxlint: 1.25.0
|
||||
eslint-plugin-prettier: ^5.5.4
|
||||
eslint-plugin-storybook: ^9.1.6
|
||||
eslint-plugin-unused-imports: ^4.2.0
|
||||
eslint-plugin-vue: ^10.4.0
|
||||
eslint-plugin-storybook: ^9.1.16
|
||||
eslint-plugin-unused-imports: ^4.3.0
|
||||
eslint-plugin-vue: ^10.6.2
|
||||
firebase: ^11.6.0
|
||||
globals: ^15.9.0
|
||||
happy-dom: ^15.11.0
|
||||
@@ -64,29 +64,29 @@ catalog:
|
||||
jiti: 2.4.2
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
lint-staged: ^15.5.2
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 21.4.1
|
||||
oxlint: ^1.25.0
|
||||
oxlint-tsgolint: ^0.4.0
|
||||
oxlint: ^1.32.0
|
||||
oxlint-tsgolint: ^0.8.4
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.6.2
|
||||
prettier: ^3.7.4
|
||||
pretty-bytes: ^7.1.0
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
rollup-plugin-visualizer: ^6.0.4
|
||||
storybook: ^9.1.6
|
||||
stylelint: ^16.24.0
|
||||
storybook: ^9.1.16
|
||||
stylelint: ^16.26.1
|
||||
tailwindcss: ^4.1.12
|
||||
tailwindcss-primeui: ^0.6.1
|
||||
tsx: ^4.15.6
|
||||
tw-animate-css: ^1.3.8
|
||||
typegpu: ^0.8.2
|
||||
typescript: ^5.9.2
|
||||
typescript-eslint: ^8.44.0
|
||||
typescript: ^5.9.3
|
||||
typescript-eslint: ^8.49.0
|
||||
unplugin-icons: ^0.22.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^0.28.0
|
||||
@@ -100,7 +100,7 @@ catalog:
|
||||
vue-eslint-parser: ^10.2.0
|
||||
vue-i18n: ^9.14.3
|
||||
vue-router: ^4.4.3
|
||||
vue-tsc: ^3.0.7
|
||||
vue-tsc: ^3.1.8
|
||||
vuefire: ^3.2.1
|
||||
yjs: ^13.6.27
|
||||
zod: ^3.23.8
|
||||
|
||||
@@ -55,7 +55,6 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { subcategories } = defineProps<{
|
||||
commands: ComfyCommandImpl[]
|
||||
subcategories: Record<string, ComfyCommandImpl[]>
|
||||
}>()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
class="icon-[lucide--triangle-alert] text-warning-background"
|
||||
/>
|
||||
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
|
||||
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
|
||||
<Tag v-if="item.isBlueprint" value="Blueprint" severity="primary" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
</a>
|
||||
<Menu
|
||||
|
||||
@@ -41,7 +41,7 @@ const {
|
||||
inputAttrs?: Record<string, string>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])
|
||||
const emit = defineEmits(['edit', 'cancel'])
|
||||
const inputValue = ref<string>(modelValue)
|
||||
const inputRef = ref<InstanceType<typeof InputText> | undefined>()
|
||||
const isCanceling = ref(false)
|
||||
|
||||
@@ -11,7 +11,6 @@ import InputText from 'primevue/inputtext'
|
||||
|
||||
const modelValue = defineModel<string>('modelValue')
|
||||
defineProps<{
|
||||
defaultValue?: string
|
||||
label?: string
|
||||
}>()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
icon="pi pi-exclamation-circle"
|
||||
:title="title"
|
||||
:message="error.exceptionMessage"
|
||||
:text-class="'break-words max-w-[60vw]'"
|
||||
text-class="break-words max-w-[60vw]"
|
||||
/>
|
||||
<template v-if="error.extensionFile">
|
||||
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
|
||||
|
||||
@@ -465,9 +465,8 @@ onMounted(async () => {
|
||||
await workflowPersistence.loadTemplateFromUrlIfPresent()
|
||||
|
||||
// Initialize release store to fetch releases from comfy-api (fire-and-forget)
|
||||
const { useReleaseStore } = await import(
|
||||
'@/platform/updates/common/releaseStore'
|
||||
)
|
||||
const { useReleaseStore } =
|
||||
await import('@/platform/updates/common/releaseStore')
|
||||
const releaseStore = useReleaseStore()
|
||||
void releaseStore.initialize()
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
ref="zoomButton"
|
||||
v-tooltip.top="t('zoomControls.label')"
|
||||
severity="secondary"
|
||||
:label="t('zoomControls.label')"
|
||||
@@ -56,7 +55,6 @@
|
||||
<div class="h-[27px] w-[1px] self-center bg-node-divider" />
|
||||
|
||||
<Button
|
||||
ref="minimapButton"
|
||||
v-tooltip.top="minimapTooltip"
|
||||
severity="secondary"
|
||||
:aria-label="minimapTooltip"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<Popover
|
||||
ref="popover"
|
||||
:append-to="'body'"
|
||||
append-to="body"
|
||||
:auto-z-index="true"
|
||||
:base-z-index="1000"
|
||||
:dismissable="true"
|
||||
|
||||
@@ -21,7 +21,6 @@ import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const props = defineProps<{
|
||||
widget?: object
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
<div v-show="activeCategory" class="rounded-lg bg-smoke-700/30">
|
||||
<SceneControls
|
||||
v-if="showSceneControls"
|
||||
ref="sceneControlsRef"
|
||||
v-model:show-grid="sceneConfig!.showGrid"
|
||||
v-model:background-color="sceneConfig!.backgroundColor"
|
||||
v-model:background-image="sceneConfig!.backgroundImage"
|
||||
@@ -46,28 +45,24 @@
|
||||
|
||||
<ModelControls
|
||||
v-if="showModelControls"
|
||||
ref="modelControlsRef"
|
||||
v-model:material-mode="modelConfig!.materialMode"
|
||||
v-model:up-direction="modelConfig!.upDirection"
|
||||
/>
|
||||
|
||||
<CameraControls
|
||||
v-if="showCameraControls"
|
||||
ref="cameraControlsRef"
|
||||
v-model:camera-type="cameraConfig!.cameraType"
|
||||
v-model:fov="cameraConfig!.fov"
|
||||
/>
|
||||
|
||||
<LightControls
|
||||
v-if="showLightControls"
|
||||
ref="lightControlsRef"
|
||||
v-model:light-intensity="lightConfig!.intensity"
|
||||
v-model:material-mode="modelConfig!.materialMode"
|
||||
/>
|
||||
|
||||
<ExportControls
|
||||
v-if="showExportControls"
|
||||
ref="exportControlsRef"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span v-if="props.mode === 'allFailed'" class="inline-flex items-center">
|
||||
<i
|
||||
class="ml-1 icon-[lucide--circle-alert] block size-4 leading-none"
|
||||
:class="'text-destructive-background'"
|
||||
class="ml-1 icon-[lucide--circle-alert] block size-4 leading-none text-destructive-background"
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -64,8 +64,7 @@ export const RunningWithCurrent: Story = {
|
||||
state: 'running',
|
||||
title: 'Generating image',
|
||||
progressTotalPercent: 66,
|
||||
progressCurrentPercent: 10,
|
||||
runningNodeName: 'KSampler'
|
||||
progressCurrentPercent: 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +225,6 @@ const props = withDefaults(
|
||||
showMenu?: boolean
|
||||
progressTotalPercent?: number
|
||||
progressCurrentPercent?: number
|
||||
runningNodeName?: string
|
||||
activeDetailsId?: string | null
|
||||
}>(),
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { ClassValue } from '@/utils/tailwindUtil'
|
||||
const props = defineProps<{
|
||||
nodeTitle: string
|
||||
widgetName: string
|
||||
isShown?: boolean
|
||||
isDraggable?: boolean
|
||||
isPhysical?: boolean
|
||||
class?: ClassValue
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
auto-option-focus
|
||||
force-selection
|
||||
multiple
|
||||
:option-label="'display_name'"
|
||||
option-label="display_name"
|
||||
@complete="search($event.query)"
|
||||
@option-select="onAddNode($event.value)"
|
||||
@focused-option-changed="setHoverSuggestion($event)"
|
||||
|
||||
@@ -59,7 +59,6 @@ interface WorkflowOption {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
class?: string
|
||||
workflowOption: WorkflowOption
|
||||
}>()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
SliderRootProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { refDebounced, watchDebounced } from '@vueuse/core'
|
||||
import Fuse from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import {
|
||||
DEFAULT_TEMPLATE_FUSE_CONFIG,
|
||||
TEMPLATE_FUSE_SETTINGS_KEY,
|
||||
buildTemplateFuseOptions
|
||||
} from '@/platform/workflow/templates/utils/templateFuseOptions'
|
||||
import { TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY } from '@/platform/workflow/templates/utils/templateSearchLabInjection'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
|
||||
export function useTemplateFiltering(
|
||||
@@ -13,15 +19,19 @@ export function useTemplateFiltering(
|
||||
) {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const injectedSearchQuery = inject<Ref<string> | null>(
|
||||
TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY,
|
||||
null
|
||||
)
|
||||
const searchQuery = injectedSearchQuery ?? ref('')
|
||||
const selectedModels = ref<string[]>(
|
||||
settingStore.get('Comfy.Templates.SelectedModels')
|
||||
settingStore.get('Comfy.Templates.SelectedModels') ?? []
|
||||
)
|
||||
const selectedUseCases = ref<string[]>(
|
||||
settingStore.get('Comfy.Templates.SelectedUseCases')
|
||||
settingStore.get('Comfy.Templates.SelectedUseCases') ?? []
|
||||
)
|
||||
const selectedRunsOn = ref<string[]>(
|
||||
settingStore.get('Comfy.Templates.SelectedRunsOn')
|
||||
settingStore.get('Comfy.Templates.SelectedRunsOn') ?? []
|
||||
)
|
||||
const sortBy = ref<
|
||||
| 'default'
|
||||
@@ -36,21 +46,24 @@ export function useTemplateFiltering(
|
||||
return Array.isArray(templateData) ? templateData : []
|
||||
})
|
||||
|
||||
// Fuse.js configuration for fuzzy search
|
||||
const fuseOptions = {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.3 },
|
||||
{ name: 'title', weight: 0.3 },
|
||||
{ name: 'description', weight: 0.2 },
|
||||
{ name: 'tags', weight: 0.1 },
|
||||
{ name: 'models', weight: 0.1 }
|
||||
],
|
||||
threshold: 0.4,
|
||||
includeScore: true,
|
||||
includeMatches: true
|
||||
}
|
||||
const fuseConfig = computed(
|
||||
() =>
|
||||
settingStore.get(TEMPLATE_FUSE_SETTINGS_KEY) ??
|
||||
DEFAULT_TEMPLATE_FUSE_CONFIG
|
||||
)
|
||||
|
||||
const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions))
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 50)
|
||||
|
||||
const fuse = computed(
|
||||
() =>
|
||||
new Fuse(
|
||||
templatesArray.value,
|
||||
buildTemplateFuseOptions<TemplateInfo>({
|
||||
config: fuseConfig.value,
|
||||
query: debouncedSearchQuery.value.trim().toLowerCase()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const availableModels = computed(() => {
|
||||
const modelSet = new Set<string>()
|
||||
@@ -76,8 +89,6 @@ export function useTemplateFiltering(
|
||||
return ['ComfyUI', 'External or Remote API']
|
||||
})
|
||||
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 50)
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
if (!debouncedSearchQuery.value.trim()) {
|
||||
return templatesArray.value
|
||||
|
||||
@@ -249,9 +249,7 @@ const link_bounding = new Rectangle()
|
||||
* This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
|
||||
* Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
|
||||
*/
|
||||
export class LGraphCanvas
|
||||
implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
{
|
||||
export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap> {
|
||||
static DEFAULT_BACKGROUND_IMAGE =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* Temporary workaround until downstream consumers migrate to Map.
|
||||
* A brittle wrapper with many flaws, but should be fine for simple maps using int indexes.
|
||||
*/
|
||||
export class MapProxyHandler<V>
|
||||
implements ProxyHandler<Map<number | string, V>>
|
||||
{
|
||||
export class MapProxyHandler<V> implements ProxyHandler<
|
||||
Map<number | string, V>
|
||||
> {
|
||||
getOwnPropertyDescriptor(
|
||||
target: Map<number | string, V>,
|
||||
p: string | symbol
|
||||
|
||||
@@ -81,9 +81,9 @@ export interface CustomEventDispatcher<
|
||||
* ```
|
||||
*/
|
||||
export class CustomEventTarget<
|
||||
EventMap extends Record<Keys, unknown>,
|
||||
Keys extends keyof EventMap & string = keyof EventMap & string
|
||||
>
|
||||
EventMap extends Record<Keys, unknown>,
|
||||
Keys extends keyof EventMap & string = keyof EventMap & string
|
||||
>
|
||||
extends EventTarget
|
||||
implements ICustomEventTarget<EventMap, Keys>
|
||||
{
|
||||
|
||||
@@ -380,8 +380,10 @@ interface IContextMenuBase {
|
||||
}
|
||||
|
||||
/** ContextMenu */
|
||||
export interface IContextMenuOptions<TValue = unknown, TExtra = unknown>
|
||||
extends IContextMenuBase {
|
||||
export interface IContextMenuOptions<
|
||||
TValue = unknown,
|
||||
TExtra = unknown
|
||||
> extends IContextMenuBase {
|
||||
ignore_item_callbacks?: boolean
|
||||
parentMenu?: ContextMenu<TValue>
|
||||
event?: MouseEvent
|
||||
@@ -426,13 +428,15 @@ export interface IContextMenuValue<
|
||||
): void | boolean | Promise<void | boolean>
|
||||
}
|
||||
|
||||
interface IContextMenuSubmenu<TValue = unknown>
|
||||
extends IContextMenuOptions<TValue> {
|
||||
interface IContextMenuSubmenu<
|
||||
TValue = unknown
|
||||
> extends IContextMenuOptions<TValue> {
|
||||
options: ConstructorParameters<typeof ContextMenu<TValue>>[0]
|
||||
}
|
||||
|
||||
export interface ContextMenuDivElement<TValue = unknown>
|
||||
extends HTMLDivElement {
|
||||
export interface ContextMenuDivElement<
|
||||
TValue = unknown
|
||||
> extends HTMLDivElement {
|
||||
value?: string | IContextMenuValue<TValue>
|
||||
onclick_callback?: never
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import type { SubgraphInput } from './SubgraphInput'
|
||||
import type { SubgraphOutput } from './SubgraphOutput'
|
||||
|
||||
export abstract class SubgraphIONodeBase<
|
||||
TSlot extends SubgraphInput | SubgraphOutput
|
||||
>
|
||||
TSlot extends SubgraphInput | SubgraphOutput
|
||||
>
|
||||
implements Positionable, Hoverable, Serialisable<ExportedSubgraphIONode>
|
||||
{
|
||||
static margin = 10
|
||||
|
||||
@@ -46,9 +46,7 @@ export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent {}
|
||||
|
||||
/** MouseEvent with canvasX/Y and deltaX/Y properties */
|
||||
interface CanvasMouseEvent
|
||||
extends MouseEvent,
|
||||
Readonly<CanvasPointerExtensions>,
|
||||
LegacyMouseEvent {}
|
||||
extends MouseEvent, Readonly<CanvasPointerExtensions>, LegacyMouseEvent {}
|
||||
|
||||
export type CanvasEventDetail =
|
||||
| GenericEventDetail
|
||||
|
||||
@@ -94,27 +94,32 @@ export interface INumericWidget extends IBaseWidget<number, 'number'> {
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface ISliderWidget
|
||||
extends IBaseWidget<number, 'slider', IWidgetSliderOptions> {
|
||||
export interface ISliderWidget extends IBaseWidget<
|
||||
number,
|
||||
'slider',
|
||||
IWidgetSliderOptions
|
||||
> {
|
||||
type: 'slider'
|
||||
value: number
|
||||
marker?: number
|
||||
}
|
||||
|
||||
export interface IKnobWidget
|
||||
extends IBaseWidget<number, 'knob', IWidgetKnobOptions> {
|
||||
export interface IKnobWidget extends IBaseWidget<
|
||||
number,
|
||||
'knob',
|
||||
IWidgetKnobOptions
|
||||
> {
|
||||
type: 'knob'
|
||||
value: number
|
||||
options: IWidgetKnobOptions
|
||||
}
|
||||
|
||||
/** Avoids the type issues with the legacy IComboWidget type */
|
||||
export interface IStringComboWidget
|
||||
extends IBaseWidget<
|
||||
string,
|
||||
'combo',
|
||||
RequiredProps<IWidgetOptions<string[]>, 'values'>
|
||||
> {
|
||||
export interface IStringComboWidget extends IBaseWidget<
|
||||
string,
|
||||
'combo',
|
||||
RequiredProps<IWidgetOptions<string[]>, 'values'>
|
||||
> {
|
||||
type: 'combo'
|
||||
value: string
|
||||
}
|
||||
@@ -125,25 +130,29 @@ type ComboWidgetValues =
|
||||
| ((widget?: IComboWidget, node?: LGraphNode) => string[])
|
||||
|
||||
/** A combo-box widget (dropdown, select, etc) */
|
||||
export interface IComboWidget
|
||||
extends IBaseWidget<
|
||||
string | number,
|
||||
'combo',
|
||||
RequiredProps<IWidgetOptions<ComboWidgetValues>, 'values'>
|
||||
> {
|
||||
export interface IComboWidget extends IBaseWidget<
|
||||
string | number,
|
||||
'combo',
|
||||
RequiredProps<IWidgetOptions<ComboWidgetValues>, 'values'>
|
||||
> {
|
||||
type: 'combo'
|
||||
value: string | number
|
||||
}
|
||||
|
||||
/** A widget with a string value */
|
||||
export interface IStringWidget
|
||||
extends IBaseWidget<string, 'string' | 'text', IWidgetOptions<string[]>> {
|
||||
export interface IStringWidget extends IBaseWidget<
|
||||
string,
|
||||
'string' | 'text',
|
||||
IWidgetOptions<string[]>
|
||||
> {
|
||||
type: 'string' | 'text'
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface IButtonWidget
|
||||
extends IBaseWidget<string | undefined, 'button'> {
|
||||
export interface IButtonWidget extends IBaseWidget<
|
||||
string | undefined,
|
||||
'button'
|
||||
> {
|
||||
type: 'button'
|
||||
value: string | undefined
|
||||
clicked: boolean
|
||||
@@ -181,15 +190,19 @@ interface IImageWidget extends IBaseWidget<string, 'image'> {
|
||||
}
|
||||
|
||||
/** Tree select widget for hierarchical selection */
|
||||
export interface ITreeSelectWidget
|
||||
extends IBaseWidget<string | string[], 'treeselect'> {
|
||||
export interface ITreeSelectWidget extends IBaseWidget<
|
||||
string | string[],
|
||||
'treeselect'
|
||||
> {
|
||||
type: 'treeselect'
|
||||
value: string | string[]
|
||||
}
|
||||
|
||||
/** Multi-select widget for selecting multiple options */
|
||||
export interface IMultiSelectWidget
|
||||
extends IBaseWidget<string[], 'multiselect'> {
|
||||
export interface IMultiSelectWidget extends IBaseWidget<
|
||||
string[],
|
||||
'multiselect'
|
||||
> {
|
||||
type: 'multiselect'
|
||||
value: string[]
|
||||
}
|
||||
@@ -207,19 +220,20 @@ export interface IGalleriaWidget extends IBaseWidget<string[], 'galleria'> {
|
||||
}
|
||||
|
||||
/** Image comparison widget for comparing two images side by side */
|
||||
export interface IImageCompareWidget
|
||||
extends IBaseWidget<string[], 'imagecompare'> {
|
||||
export interface IImageCompareWidget extends IBaseWidget<
|
||||
string[],
|
||||
'imagecompare'
|
||||
> {
|
||||
type: 'imagecompare'
|
||||
value: string[]
|
||||
}
|
||||
|
||||
/** Select button widget for selecting from a group of buttons */
|
||||
export interface ISelectButtonWidget
|
||||
extends IBaseWidget<
|
||||
string,
|
||||
'selectbutton',
|
||||
RequiredProps<IWidgetOptions<string[]>, 'values'>
|
||||
> {
|
||||
export interface ISelectButtonWidget extends IBaseWidget<
|
||||
string,
|
||||
'selectbutton',
|
||||
RequiredProps<IWidgetOptions<string[]>, 'values'>
|
||||
> {
|
||||
type: 'selectbutton'
|
||||
value: string
|
||||
}
|
||||
@@ -230,8 +244,11 @@ export interface ITextareaWidget extends IBaseWidget<string, 'textarea'> {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface IAssetWidget
|
||||
extends IBaseWidget<string, 'asset', IWidgetOptions<string[]>> {
|
||||
export interface IAssetWidget extends IBaseWidget<
|
||||
string,
|
||||
'asset',
|
||||
IWidgetOptions<string[]>
|
||||
> {
|
||||
type: 'asset'
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ export interface WidgetEventOptions {
|
||||
canvas: LGraphCanvas
|
||||
}
|
||||
|
||||
export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
implements IBaseWidget
|
||||
{
|
||||
export abstract class BaseWidget<
|
||||
TWidget extends IBaseWidget = IBaseWidget
|
||||
> implements IBaseWidget {
|
||||
/** From node edge to widget edge */
|
||||
static margin = 15
|
||||
/** From widget edge to tip of arrow button */
|
||||
|
||||
@@ -880,6 +880,153 @@
|
||||
"searchPlaceholder": "Search..."
|
||||
}
|
||||
},
|
||||
"templateSearchLab": {
|
||||
"title": "Template Search Tuning Lab",
|
||||
"subtitle": "Fine-tune Fuse.js options and instantly preview template results.",
|
||||
"description": "Use real template data to decide how strict or fuzzy searches like \"Wan\" and \"Animate\" should feel before handing settings to engineering.",
|
||||
"reset": "Reset to recommended defaults",
|
||||
"configCopied": "Copied",
|
||||
"configCopy": "Copy JSON",
|
||||
"searchLabel": "Type a template, model, or tag",
|
||||
"searchPlaceholder": "Search templates (try \"Wan\")",
|
||||
"samplesLabel": "Quick queries",
|
||||
"previewTitle": "Live Preview",
|
||||
"previewCount": "{count} matches",
|
||||
"loadingTemplates": "Loading template catalog...",
|
||||
"previewEmptyState": "Start typing or tap a sample query to preview matches.",
|
||||
"previewNoResults": "No templates match \"{query}\".",
|
||||
"scoreLabel": "Score",
|
||||
"matchLabel": "Match found in {field}",
|
||||
"unknownField": "unknown field",
|
||||
"matchFallback": "Enable includeMatches to view highlighted indices.",
|
||||
"configHeading": "Fuse option snapshot",
|
||||
"configSubheading": "Copy this block into the composable or share it with engineering.",
|
||||
"additionalReadingTitle": "Helpful reading",
|
||||
"additionalReadingSubtitle": "Deep dives that explain how scoring and extended syntax work.",
|
||||
"previewSummaryIdle": "Waiting for a query…",
|
||||
"previewSummaryEmpty": "Search ran but no templates matched.",
|
||||
"previewSummaryActive": "Showing {count} of {total} templates.",
|
||||
"docLinkLabel": "Docs",
|
||||
"links": {
|
||||
"apiDocs": "Fuse option reference",
|
||||
"scoringTheory": "Scoring theory explainer",
|
||||
"extendedSearch": "Extended search syntax"
|
||||
},
|
||||
"dialogPreviewTitle": "Live Templates Dialog",
|
||||
"dialogPreviewSubtitle": "Same component designers see in production, updated in real time as you tweak settings.",
|
||||
"optionCountSuffix": "settings",
|
||||
"optionGroups": {
|
||||
"basic": {
|
||||
"title": "Basic options",
|
||||
"description": "Quick toggles that control how literal or fuzzy the search feels."
|
||||
},
|
||||
"fuzzy": {
|
||||
"title": "Fuzzy matching",
|
||||
"description": "Control how tolerant we are of typos and offsets."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Ranking strategies, extended syntax, and scoring heuristics."
|
||||
}
|
||||
},
|
||||
"sortLabel": "Sort strategy",
|
||||
"sortDescription": "Override Fuse's default ordering when designers want exact or prefix matches before fuzzy ones.",
|
||||
"sortExample": "Use \"Exact title first\" when Wan-branded templates must show before descriptions that merely mention \"Wan\".",
|
||||
"sortModes": {
|
||||
"score": "Fuse score (default)",
|
||||
"exact": "Exact title or name first",
|
||||
"prefix": "Prefix / word-start boost"
|
||||
},
|
||||
"getFnLabel": "Collection accessor",
|
||||
"getFnDescription": "Control how array fields such as tags or models are flattened before scoring.",
|
||||
"getFnExample": "Flatten tags/models if designers want \"Wan\" to match the model list even when the title differs.",
|
||||
"getFnModes": {
|
||||
"default": "Fuse default accessor",
|
||||
"flatten": "Flatten arrays into readable strings"
|
||||
},
|
||||
"keysHeading": "Keys & weights",
|
||||
"keysDescription": "Pick which template fields feed the Fuse index and how much they matter.",
|
||||
"keysHelper": "Weights are relative—Fuse normalizes them automatically.",
|
||||
"keysAddLabel": "Field to add",
|
||||
"keysWeightLabel": "Weight",
|
||||
"keysAddButton": "Add field",
|
||||
"keysOptions": {
|
||||
"name": "Raw name",
|
||||
"title": "Localized title",
|
||||
"description": "Description",
|
||||
"tags": "Tags",
|
||||
"models": "Models",
|
||||
"useCase": "Use case",
|
||||
"sourceModule": "Source module"
|
||||
},
|
||||
"keys": {
|
||||
"nameDescription": "Matches internal slugs such as wan_image_diffusion.",
|
||||
"titleDescription": "Searches the display title (localized), e.g., Wan Diffusion Starter.",
|
||||
"descriptionDescription": "Looks through the marketing copy for keywords like \"face retouch\".",
|
||||
"tagsDescription": "Covers curated tags (portrait, anime, product, etc.).",
|
||||
"modelsDescription": "Matches referenced model names such as Wan or SDXL.",
|
||||
"customDescription": "Custom weighting for {field}."
|
||||
},
|
||||
"removeLabel": "Remove",
|
||||
"options": {
|
||||
"isCaseSensitive": {
|
||||
"description": "Respect letter casing when matching template text.",
|
||||
"example": "Turn on if \"WAN\" should not match \"Wan Diffusion\" in lowercase."
|
||||
},
|
||||
"ignoreDiacritics": {
|
||||
"description": "Treat accents and diacritics as plain letters.",
|
||||
"example": "Let \"anime\" match templates tagged \"animé\" without needing the accent."
|
||||
},
|
||||
"includeScore": {
|
||||
"description": "Expose the Fuse score so you can see how confident each hit is.",
|
||||
"example": "Helps compare how much higher \"Wan Diffusion\" scores than \"Glow Portrait\" for the same query."
|
||||
},
|
||||
"includeMatches": {
|
||||
"description": "Return the character indices of each match for highlighting.",
|
||||
"example": "Needed when you want to visually highlight \"Wan\" inside descriptions or tags."
|
||||
},
|
||||
"minMatchCharLength": {
|
||||
"description": "Ignore matches shorter than this many characters.",
|
||||
"example": "Set to 2 so a single \"w\" typed by accident does not reshuffle the template list."
|
||||
},
|
||||
"shouldSort": {
|
||||
"description": "Let Fuse sort by score. Turn off to keep the original grouping order.",
|
||||
"example": "Disable when designers want curated ordering even while filtering by text."
|
||||
},
|
||||
"findAllMatches": {
|
||||
"description": "Continue searching after a perfect hit to surface every occurrence.",
|
||||
"example": "Use when multiple models inside the description should all highlight \"Wan\" segments."
|
||||
},
|
||||
"location": {
|
||||
"description": "Bias toward matches near this index in the text.",
|
||||
"example": "Keep near 0 if template titles should matter more than long descriptions."
|
||||
},
|
||||
"threshold": {
|
||||
"description": "Overall fuzziness. 0 is exact, 1 matches almost anything.",
|
||||
"example": "0.2 is strict enough that \"Wan\" surfaces Wan-branded templates before unrelated blurbs."
|
||||
},
|
||||
"distance": {
|
||||
"description": "How far from the expected location a match can drift before being ignored.",
|
||||
"example": "Lower values keep \"Wan\" from matching a paragraph hundreds of characters away."
|
||||
},
|
||||
"ignoreLocation": {
|
||||
"description": "When true, disable the location + distance penalty entirely.",
|
||||
"example": "Great for template data where matches can live anywhere within descriptions or tags."
|
||||
},
|
||||
"useExtendedSearch": {
|
||||
"description": "Allow ^=, =, and other extended search syntax for power users.",
|
||||
"example": "Type ^=wan to show only templates whose names start with Wan."
|
||||
},
|
||||
"ignoreFieldNorm": {
|
||||
"description": "Ignore the penalty for long fields.",
|
||||
"example": "Enable when long descriptions should not count against simple titles."
|
||||
},
|
||||
"fieldNormWeight": {
|
||||
"description": "Scale how much field length matters in scoring.",
|
||||
"example": "Set to 0 when you only care that \"Wan\" exists somewhere, no matter how wordy the field is."
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
@@ -2392,4 +2539,4 @@
|
||||
"recentReleases": "Recent releases",
|
||||
"helpCenterMenu": "Help Center Menu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ import { i18n } from './i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
if (isCloud) {
|
||||
const { loadRemoteConfig } = await import(
|
||||
'@/platform/remoteConfig/remoteConfig'
|
||||
)
|
||||
const { loadRemoteConfig } =
|
||||
await import('@/platform/remoteConfig/remoteConfig')
|
||||
await loadRemoteConfig()
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ import { OnCloseKey } from '@/types/widgetTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeType?: string
|
||||
inputName?: string
|
||||
onSelect?: (asset: AssetItem) => void
|
||||
onClose?: () => void
|
||||
showLeftPanel?: boolean
|
||||
|
||||
@@ -44,7 +44,6 @@ TODO: Extract checkbox pattern into reusable Checkbox component
|
||||
<script setup lang="ts">
|
||||
const { mediaTypeFilters } = defineProps<{
|
||||
mediaTypeFilters: string[]
|
||||
close: () => void
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<Popover
|
||||
ref="popover"
|
||||
:append-to="'body'"
|
||||
append-to="body"
|
||||
:auto-z-index="true"
|
||||
:base-z-index="1000"
|
||||
:dismissable="true"
|
||||
|
||||
@@ -16,12 +16,11 @@ import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
|
||||
@@ -14,12 +14,11 @@ import { computed } from 'vue'
|
||||
|
||||
import { getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
|
||||
@@ -13,12 +13,11 @@ import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@mouseleave="isHovered = false"
|
||||
>
|
||||
<video
|
||||
ref="videoRef"
|
||||
:controls="shouldShowControls"
|
||||
preload="metadata"
|
||||
autoplay
|
||||
@@ -27,20 +26,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
play: [assetId: string]
|
||||
videoPlayingStateChanged: [isPlaying: boolean]
|
||||
videoControlsChanged: [showControls: boolean]
|
||||
}>()
|
||||
|
||||
const videoRef = ref<HTMLVideoElement>()
|
||||
const isHovered = ref(false)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
beforeEnter: async (to, _from, next) => {
|
||||
// Only redirect if not explicitly switching accounts
|
||||
if (!to.query.switchAccount) {
|
||||
const { useCurrentUser } = await import(
|
||||
'@/composables/auth/useCurrentUser'
|
||||
)
|
||||
const { useCurrentUser } =
|
||||
await import('@/composables/auth/useCurrentUser')
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
if (isLoggedIn.value) {
|
||||
|
||||
@@ -28,9 +28,7 @@ export const useSubscriptionDialog = () => {
|
||||
key: DIALOG_KEY,
|
||||
component: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vue'
|
||||
)
|
||||
import('@/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vue')
|
||||
),
|
||||
props: {
|
||||
onClose: hide
|
||||
|
||||
@@ -100,9 +100,7 @@ export function useSettingUI(
|
||||
},
|
||||
component: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/platform/cloud/subscription/components/SubscriptionPanel.vue'
|
||||
)
|
||||
import('@/platform/cloud/subscription/components/SubscriptionPanel.vue')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
206
src/platform/workflow/templates/utils/templateFuseOptions.ts
Normal file
206
src/platform/workflow/templates/utils/templateFuseOptions.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { cloneDeep } from 'es-toolkit/compat'
|
||||
import type {
|
||||
IFuseOptions,
|
||||
FuseSortFunction,
|
||||
FuseSortFunctionArg
|
||||
} from 'fuse.js'
|
||||
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
|
||||
export const TEMPLATE_FUSE_SETTINGS_KEY = 'Comfy.Templates.FuseOverrides'
|
||||
|
||||
export type TemplateFuseSortMode = 'score' | 'exact' | 'prefix'
|
||||
export type TemplateFuseGetMode = 'default' | 'flatten'
|
||||
|
||||
export interface TemplateFuseOptionState {
|
||||
isCaseSensitive: boolean
|
||||
ignoreDiacritics: boolean
|
||||
includeScore: boolean
|
||||
includeMatches: boolean
|
||||
minMatchCharLength: number
|
||||
shouldSort: boolean
|
||||
findAllMatches: boolean
|
||||
location: number
|
||||
threshold: number
|
||||
distance: number
|
||||
ignoreLocation: boolean
|
||||
useExtendedSearch: boolean
|
||||
ignoreFieldNorm: boolean
|
||||
fieldNormWeight: number
|
||||
}
|
||||
|
||||
export interface TemplateFuseKeyConfig {
|
||||
path: string
|
||||
weight: number
|
||||
}
|
||||
|
||||
export interface TemplateFuseConfig {
|
||||
options: TemplateFuseOptionState
|
||||
keys: TemplateFuseKeyConfig[]
|
||||
sortMode: TemplateFuseSortMode
|
||||
getFnMode: TemplateFuseGetMode
|
||||
}
|
||||
|
||||
const DEFAULT_TEMPLATE_FUSE_OPTIONS: TemplateFuseOptionState = {
|
||||
isCaseSensitive: false,
|
||||
ignoreDiacritics: true,
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 1,
|
||||
shouldSort: true,
|
||||
findAllMatches: false,
|
||||
location: 0,
|
||||
threshold: 0.4,
|
||||
distance: 100,
|
||||
ignoreLocation: false,
|
||||
useExtendedSearch: false,
|
||||
ignoreFieldNorm: false,
|
||||
fieldNormWeight: 1
|
||||
}
|
||||
|
||||
const DEFAULT_TEMPLATE_FUSE_KEYS: TemplateFuseKeyConfig[] = [
|
||||
{ path: 'name', weight: 0.3 },
|
||||
{ path: 'title', weight: 0.3 },
|
||||
{ path: 'description', weight: 0.2 },
|
||||
{ path: 'tags', weight: 0.1 },
|
||||
{ path: 'models', weight: 0.1 }
|
||||
]
|
||||
|
||||
export const DEFAULT_TEMPLATE_FUSE_CONFIG: TemplateFuseConfig = {
|
||||
options: DEFAULT_TEMPLATE_FUSE_OPTIONS,
|
||||
keys: DEFAULT_TEMPLATE_FUSE_KEYS,
|
||||
sortMode: 'score',
|
||||
getFnMode: 'default'
|
||||
}
|
||||
|
||||
type TemplateLike = TemplateInfo & {
|
||||
localizedTitle?: string
|
||||
}
|
||||
|
||||
type BuildOptionsParams = {
|
||||
config?: TemplateFuseConfig | null
|
||||
query?: string
|
||||
}
|
||||
|
||||
export function buildTemplateFuseOptions<T extends TemplateLike>(
|
||||
params?: BuildOptionsParams
|
||||
): IFuseOptions<T> {
|
||||
const baseConfig = params?.config ?? DEFAULT_TEMPLATE_FUSE_CONFIG
|
||||
const normalizedKeys =
|
||||
baseConfig.keys.length > 0 ? baseConfig.keys : DEFAULT_TEMPLATE_FUSE_KEYS
|
||||
|
||||
return {
|
||||
...cloneDeep(baseConfig.options),
|
||||
keys: normalizedKeys.map((entry) => ({
|
||||
name: entry.path,
|
||||
weight: entry.weight
|
||||
})),
|
||||
getFn: buildGetFn<T>(baseConfig.getFnMode),
|
||||
sortFn: buildSortFn(baseConfig.sortMode, params?.query)
|
||||
}
|
||||
}
|
||||
|
||||
function buildGetFn<T extends TemplateLike>(mode: TemplateFuseGetMode) {
|
||||
if (mode !== 'flatten') {
|
||||
return undefined
|
||||
}
|
||||
return (obj: T, path: string | string[]) => {
|
||||
if (Array.isArray(path)) {
|
||||
return path.map((segment) => stringifyValue(obj, segment))
|
||||
}
|
||||
return stringifyValue(obj, path)
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyValue(object: TemplateLike, path: string) {
|
||||
const raw = resolvePath(object, path)
|
||||
if (raw == null) return ''
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
.map((entry) =>
|
||||
typeof entry === 'string' ? entry : JSON.stringify(entry)
|
||||
)
|
||||
.join(' ')
|
||||
}
|
||||
if (typeof raw === 'object') {
|
||||
return Object.values(raw)
|
||||
.map((value) => (typeof value === 'string' ? value : ''))
|
||||
.join(' ')
|
||||
}
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
function resolvePath(object: TemplateLike, path: string) {
|
||||
return path.split('.').reduce<unknown>((current, segment) => {
|
||||
if (current && typeof current === 'object') {
|
||||
return (current as Record<string, unknown>)[segment]
|
||||
}
|
||||
return undefined
|
||||
}, object)
|
||||
}
|
||||
|
||||
function buildSortFn(
|
||||
mode: TemplateFuseSortMode,
|
||||
query?: string
|
||||
): FuseSortFunction | undefined {
|
||||
if (mode === 'score') {
|
||||
return undefined
|
||||
}
|
||||
const normalizedQuery = query?.toLowerCase().trim()
|
||||
return (a: FuseSortFunctionArg, b: FuseSortFunctionArg) => {
|
||||
const templateA = a.item as unknown as TemplateLike
|
||||
const templateB = b.item as unknown as TemplateLike
|
||||
if (!normalizedQuery) {
|
||||
return compareScores(a, b)
|
||||
}
|
||||
|
||||
if (mode === 'exact') {
|
||||
const aExact = isExactMatch(templateA, normalizedQuery)
|
||||
const bExact = isExactMatch(templateB, normalizedQuery)
|
||||
if (aExact !== bExact) {
|
||||
return aExact ? -1 : 1
|
||||
}
|
||||
return compareScores(a, b)
|
||||
}
|
||||
|
||||
const aPrefix = prefixScore(templateA, normalizedQuery)
|
||||
const bPrefix = prefixScore(templateB, normalizedQuery)
|
||||
if (aPrefix !== bPrefix) {
|
||||
return bPrefix - aPrefix
|
||||
}
|
||||
return compareScores(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
function compareScores(a: FuseSortFunctionArg, b: FuseSortFunctionArg) {
|
||||
const aScore = typeof a.score === 'number' ? a.score : 1
|
||||
const bScore = typeof b.score === 'number' ? b.score : 1
|
||||
return aScore - bScore
|
||||
}
|
||||
|
||||
function isExactMatch(template: TemplateLike, query: string) {
|
||||
const title = formatTitle(template)
|
||||
const name = (template.name || '').toLowerCase()
|
||||
return title === query || name === query
|
||||
}
|
||||
|
||||
function prefixScore(template: TemplateLike, query: string) {
|
||||
const title = formatTitle(template)
|
||||
const name = (template.name || '').toLowerCase()
|
||||
if (title.startsWith(query) || name.startsWith(query)) {
|
||||
return 2
|
||||
}
|
||||
if (title.includes(` ${query}`) || name.includes(` ${query}`)) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function formatTitle(template: TemplateLike) {
|
||||
return (
|
||||
template.localizedTitle ||
|
||||
template.title ||
|
||||
template.name ||
|
||||
''
|
||||
).toLowerCase()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
export const TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY: InjectionKey<Ref<string>> =
|
||||
Symbol('TemplateSearchOverride')
|
||||
@@ -181,7 +181,6 @@ import NodeWidgets from './NodeWidgets.vue'
|
||||
interface LGraphNodeProps {
|
||||
nodeData: VueNodeData
|
||||
error?: string | null
|
||||
zoomLevel?: number
|
||||
}
|
||||
|
||||
const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable vue/one-component-per-file */
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<void>
|
||||
nodeId: string
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const domEl = ref<HTMLElement>()
|
||||
|
||||
@@ -14,7 +14,6 @@ import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<void>
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const canvasEl = ref()
|
||||
|
||||
@@ -92,7 +92,6 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useAudioService } from '@/services/audioService'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import { useAudioPlayback } from '../composables/audio/useAudioPlayback'
|
||||
import { useAudioRecorder } from '../composables/audio/useAudioRecorder'
|
||||
@@ -100,7 +99,6 @@ import { useAudioWaveform } from '../composables/audio/useAudioWaveform'
|
||||
import { formatTime } from '../utils/audioUtils'
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<string | number | undefined>
|
||||
readonly?: boolean
|
||||
nodeId: string
|
||||
}>()
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
<!-- Options Button -->
|
||||
<div
|
||||
v-if="showOptionsButton"
|
||||
ref="optionsButtonRef"
|
||||
role="button"
|
||||
:tabindex="0"
|
||||
:aria-label="$t('g.moreOptions')"
|
||||
@@ -155,10 +154,8 @@ const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
readonly?: boolean
|
||||
hideWhenEmpty?: boolean
|
||||
showOptionsButton?: boolean
|
||||
modelValue?: string
|
||||
nodeId?: string
|
||||
audioUrl?: string
|
||||
}>(),
|
||||
@@ -170,7 +167,6 @@ const props = withDefaults(
|
||||
// Refs
|
||||
const audioRef = ref<HTMLAudioElement>()
|
||||
const optionsMenu = ref()
|
||||
const optionsButtonRef = ref<HTMLElement>()
|
||||
const isPlaying = ref(false)
|
||||
const isMuted = ref(false)
|
||||
const volume = ref(1)
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { DropdownItem, SelectedKey } from './types'
|
||||
interface Props {
|
||||
isOpen?: boolean
|
||||
placeholder?: string
|
||||
files: File[]
|
||||
items: DropdownItem[]
|
||||
selected: Set<SelectedKey>
|
||||
maxSelectable: number
|
||||
|
||||
@@ -67,6 +67,11 @@ const router = createRouter({
|
||||
path: 'user-select',
|
||||
name: 'UserSelectView',
|
||||
component: () => import('@/views/UserSelectView.vue')
|
||||
},
|
||||
{
|
||||
path: 'designer/search-lab',
|
||||
name: 'TemplateSearchLab',
|
||||
component: () => import('@/views/templates/TemplateSearchLab.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -169,9 +174,8 @@ if (isCloud) {
|
||||
// For root path, check actual user status to handle waitlisted users
|
||||
if (!isElectron() && isLoggedIn && to.path === '/') {
|
||||
// Import auth functions dynamically to avoid circular dependency
|
||||
const { getSurveyCompletedStatus } = await import(
|
||||
'@/platform/cloud/onboarding/auth'
|
||||
)
|
||||
const { getSurveyCompletedStatus } =
|
||||
await import('@/platform/cloud/onboarding/auth')
|
||||
try {
|
||||
// Check user's actual status
|
||||
const surveyCompleted = await getSurveyCompletedStatus()
|
||||
|
||||
@@ -369,6 +369,35 @@ const zNodeBadgeMode = z.enum(
|
||||
Object.values(NodeBadgeMode) as [string, ...string[]]
|
||||
)
|
||||
|
||||
const zTemplateFuseOptionState = z.object({
|
||||
isCaseSensitive: z.boolean(),
|
||||
ignoreDiacritics: z.boolean(),
|
||||
includeScore: z.boolean(),
|
||||
includeMatches: z.boolean(),
|
||||
minMatchCharLength: z.number(),
|
||||
shouldSort: z.boolean(),
|
||||
findAllMatches: z.boolean(),
|
||||
location: z.number(),
|
||||
threshold: z.number(),
|
||||
distance: z.number(),
|
||||
ignoreLocation: z.boolean(),
|
||||
useExtendedSearch: z.boolean(),
|
||||
ignoreFieldNorm: z.boolean(),
|
||||
fieldNormWeight: z.number()
|
||||
})
|
||||
|
||||
const zTemplateFuseKeyConfig = z.object({
|
||||
path: z.string(),
|
||||
weight: z.number()
|
||||
})
|
||||
|
||||
const zTemplateFuseOverrides = z.object({
|
||||
options: zTemplateFuseOptionState,
|
||||
keys: z.array(zTemplateFuseKeyConfig),
|
||||
sortMode: z.enum(['score', 'exact', 'prefix']),
|
||||
getFnMode: z.enum(['default', 'flatten'])
|
||||
})
|
||||
|
||||
const zSettings = z.object({
|
||||
'Comfy.ColorPalette': z.string(),
|
||||
'Comfy.CustomColorPalettes': colorPalettesSchema,
|
||||
@@ -518,6 +547,7 @@ const zSettings = z.object({
|
||||
'vram-low-to-high',
|
||||
'model-size-low-to-high'
|
||||
]),
|
||||
'Comfy.Templates.FuseOverrides': zTemplateFuseOverrides,
|
||||
/** Settings used for testing */
|
||||
'test.setting': z.any(),
|
||||
'main.sub.setting.name': z.any(),
|
||||
|
||||
@@ -15,8 +15,9 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
export interface BaseDOMWidget<V extends object | string = object | string>
|
||||
extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
|
||||
export interface BaseDOMWidget<
|
||||
V extends object | string = object | string
|
||||
> extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
|
||||
// ICustomWidget properties
|
||||
type: string
|
||||
options: DOMWidgetOptions<V>
|
||||
@@ -37,8 +38,10 @@ export interface BaseDOMWidget<V extends object | string = object | string>
|
||||
/**
|
||||
* A DOM widget that wraps a custom HTML element as a litegraph widget.
|
||||
*/
|
||||
export interface DOMWidget<T extends HTMLElement, V extends object | string>
|
||||
extends BaseDOMWidget<V> {
|
||||
export interface DOMWidget<
|
||||
T extends HTMLElement,
|
||||
V extends object | string
|
||||
> extends BaseDOMWidget<V> {
|
||||
element: T
|
||||
/**
|
||||
* @deprecated Legacy property used by some extensions for customtext
|
||||
@@ -78,8 +81,9 @@ export interface ComponentWidget<
|
||||
readonly props?: P
|
||||
}
|
||||
|
||||
export interface DOMWidgetOptions<V extends object | string>
|
||||
extends IWidgetOptions {
|
||||
export interface DOMWidgetOptions<
|
||||
V extends object | string
|
||||
> extends IWidgetOptions {
|
||||
/**
|
||||
* Whether to render a placeholder rectangle when zoomed out.
|
||||
*/
|
||||
@@ -286,9 +290,9 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
}
|
||||
|
||||
export class ComponentWidgetImpl<
|
||||
V extends object | string,
|
||||
P extends ComponentWidgetCustomProps = ComponentWidgetCustomProps
|
||||
>
|
||||
V extends object | string,
|
||||
P extends ComponentWidgetCustomProps = ComponentWidgetCustomProps
|
||||
>
|
||||
extends BaseDOMWidgetImpl<V>
|
||||
implements ComponentWidget<V, P>
|
||||
{
|
||||
|
||||
@@ -535,9 +535,8 @@ export const useDialogService = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const { useSubscriptionDialog } = await import(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
)
|
||||
const { useSubscriptionDialog } =
|
||||
await import('@/platform/cloud/subscription/composables/useSubscriptionDialog')
|
||||
const { show } = useSubscriptionDialog()
|
||||
show()
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
export interface ElectronDownload
|
||||
extends Pick<DownloadState, 'url' | 'filename'> {
|
||||
export interface ElectronDownload extends Pick<
|
||||
DownloadState,
|
||||
'url' | 'filename'
|
||||
> {
|
||||
progress?: number
|
||||
savePath?: string
|
||||
status?: DownloadStatus
|
||||
|
||||
937
src/views/templates/TemplateSearchLab.vue
Normal file
937
src/views/templates/TemplateSearchLab.vue
Normal file
@@ -0,0 +1,937 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid min-h-screen gap-6 overflow-y-auto p-6 lg:grid-cols-[1.1fr_0.9fr]"
|
||||
>
|
||||
<div
|
||||
class="flex max-h-[calc(100vh-3rem)] flex-col gap-6 overflow-y-auto pr-3"
|
||||
>
|
||||
<section
|
||||
class="rounded-2xl border border-border-default bg-secondary-background p-6 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<p
|
||||
class="text-sm font-semibold uppercase tracking-wide text-muted-foreground"
|
||||
>
|
||||
{{ t('templateSearchLab.title') }}
|
||||
</p>
|
||||
<h1 class="text-2xl font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.subtitle') }}
|
||||
</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('templateSearchLab.description') }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="small"
|
||||
:label="t('templateSearchLab.reset')"
|
||||
icon="icon-[lucide--refresh-cw]"
|
||||
severity="secondary"
|
||||
@click="resetLab"
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
:label="
|
||||
copyStatus === 'copied'
|
||||
? t('templateSearchLab.configCopied')
|
||||
: t('templateSearchLab.configCopy')
|
||||
"
|
||||
:icon="
|
||||
copyStatus === 'copied'
|
||||
? 'icon-[lucide--check]'
|
||||
: 'icon-[lucide--clipboard-copy]'
|
||||
"
|
||||
:severity="copyStatus === 'error' ? 'danger' : 'secondary'"
|
||||
@click="copyOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
||||
<a
|
||||
v-for="link in resourceLinks"
|
||||
:key="link.href"
|
||||
class="inline-flex items-center gap-2 text-sm font-medium text-text-primary underline-offset-4 hover:underline"
|
||||
:href="link.href"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i :class="link.icon" />
|
||||
<span>{{ link.label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 grid gap-6 lg:grid-cols-[1.6fr_1fr]">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.searchLabel') }}
|
||||
</label>
|
||||
<SearchBox
|
||||
v-model="searchQuery"
|
||||
:placeholder="t('templateSearchLab.searchPlaceholder')"
|
||||
show-border
|
||||
/>
|
||||
<div class="flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<span class="font-medium">{{
|
||||
t('templateSearchLab.samplesLabel')
|
||||
}}</span>
|
||||
<button
|
||||
v-for="sample in sampleQueries"
|
||||
:key="sample"
|
||||
type="button"
|
||||
class="rounded-full border border-border-muted px-3 py-1 text-xs font-medium text-base-foreground transition hover:bg-base-background"
|
||||
@click="searchQuery = sample"
|
||||
>
|
||||
{{ sample }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-border-default bg-secondary-background shadow-inner"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-border-muted px-4 py-3"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.previewTitle') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ previewSummary }}
|
||||
</p>
|
||||
</div>
|
||||
<Tag v-if="searchQuery.trim().length" severity="secondary">
|
||||
{{
|
||||
t('templateSearchLab.previewCount', {
|
||||
count: previewResults.length
|
||||
})
|
||||
}}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="max-h-[420px] space-y-3 overflow-y-auto px-4 py-3">
|
||||
<template v-if="!isLoaded">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('templateSearchLab.loadingTemplates') }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="!searchQuery.trim().length">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('templateSearchLab.previewEmptyState') }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else-if="previewResults.length === 0">
|
||||
<p class="text-sm text-danger-100">
|
||||
{{
|
||||
t('templateSearchLab.previewNoResults', {
|
||||
query: searchQuery
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<article
|
||||
v-for="(result, index) in previewResults"
|
||||
:key="`${result.item.name}-${index}`"
|
||||
class="rounded-lg border border-border-muted bg-secondary-background px-4 py-3"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-start justify-between gap-2"
|
||||
>
|
||||
<div>
|
||||
<p class="text-base font-semibold text-base-foreground">
|
||||
{{ formatTemplateTitle(result.item) }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ formatTemplateMeta(result.item) }}
|
||||
</p>
|
||||
</div>
|
||||
<Tag
|
||||
v-if="typeof result.score === 'number'"
|
||||
severity="info"
|
||||
>
|
||||
{{ t('templateSearchLab.scoreLabel') }}:
|
||||
{{ formatScore(result.score) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div v-if="result.matches?.length" class="mt-3 space-y-2">
|
||||
<div
|
||||
v-for="match in result.matches"
|
||||
:key="`${match.key}-${match.refIndex}`"
|
||||
class="rounded-md border border-border-muted px-3 py-2"
|
||||
>
|
||||
<p
|
||||
class="text-xs font-semibold uppercase text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
t('templateSearchLab.matchLabel', {
|
||||
field:
|
||||
match.key || t('templateSearchLab.unknownField')
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
v-if="typeof match.value === 'string'"
|
||||
class="text-sm text-base-foreground"
|
||||
>
|
||||
<span
|
||||
v-for="(chunk, chunkIndex) in buildHighlightChunks(
|
||||
match.value,
|
||||
match.indices || []
|
||||
)"
|
||||
:key="chunkIndex"
|
||||
:class="
|
||||
chunk.isHit
|
||||
? 'rounded bg-base-background px-1 font-semibold text-base-foreground'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ chunk.text }}
|
||||
</span>
|
||||
</p>
|
||||
<p v-else class="text-xs text-muted-foreground">
|
||||
{{ formatNonStringMatch(match.value) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-else-if="designerOptions.includeMatches"
|
||||
class="mt-3 text-xs text-muted-foreground"
|
||||
>
|
||||
{{ t('templateSearchLab.matchFallback') }}
|
||||
</p>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="rounded-xl border border-border-default bg-secondary-background p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.configHeading') }}
|
||||
</p>
|
||||
<i class="icon-[lucide--sparkles] text-muted-foreground" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
{{ t('templateSearchLab.configSubheading') }}
|
||||
</p>
|
||||
<pre
|
||||
class="mt-3 max-h-64 overflow-auto rounded-lg bg-base-background p-3 text-xs leading-relaxed text-base-foreground"
|
||||
>{{ shareableConfig }}</pre
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-border-default bg-secondary-background p-4"
|
||||
>
|
||||
<p class="text-sm font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.additionalReadingTitle') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('templateSearchLab.additionalReadingSubtitle') }}
|
||||
</p>
|
||||
<ul class="mt-3 space-y-2 text-sm">
|
||||
<li
|
||||
v-for="link in deepDiveLinks"
|
||||
:key="link.href"
|
||||
class="flex items-center gap-2 text-text-primary"
|
||||
>
|
||||
<i :class="link.icon" class="text-base" />
|
||||
<a
|
||||
class="hover:underline"
|
||||
:href="link.href"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ link.label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div
|
||||
v-for="group in optionGroups"
|
||||
:key="group.key"
|
||||
class="rounded-2xl border border-border-default bg-secondary-background p-6"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-lg font-semibold text-base-foreground">
|
||||
{{ group.title }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ group.description }}
|
||||
</p>
|
||||
</div>
|
||||
<Tag severity="secondary"
|
||||
>{{ group.options.length }}
|
||||
{{ t('templateSearchLab.optionCountSuffix') }}</Tag
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="option in group.options"
|
||||
:key="option.key"
|
||||
class="rounded-xl border border-border-muted bg-secondary-background p-4"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold text-base-foreground">
|
||||
{{ option.label }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ option.description }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs font-medium text-text-primary">
|
||||
{{ option.example }}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
class="text-xs font-semibold text-text-primary underline-offset-4 hover:underline"
|
||||
:href="docLink(option.anchor)"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ t('templateSearchLab.docLinkLabel') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
<ToggleSwitch
|
||||
v-if="option.type === 'boolean'"
|
||||
v-model="
|
||||
designerOptions[option.key as keyof DesignerToggleOptions]
|
||||
"
|
||||
:input-id="`toggle-${option.key}`"
|
||||
/>
|
||||
<div v-else-if="option.type === 'number'" class="space-y-2">
|
||||
<Slider
|
||||
v-model.number="
|
||||
designerOptions[
|
||||
option.key as keyof DesignerNumericOptions
|
||||
]
|
||||
"
|
||||
:min="option.min"
|
||||
:max="option.max"
|
||||
:step="option.step"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model.number="
|
||||
designerOptions[
|
||||
option.key as keyof DesignerNumericOptions
|
||||
]
|
||||
"
|
||||
:min="option.min"
|
||||
:max="option.max"
|
||||
:step="option.step"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="option.type === 'sort'" class="space-y-2">
|
||||
<Select
|
||||
v-model="sortMode"
|
||||
:options="sortOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('templateSearchLab.sortDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="option.type === 'get'" class="space-y-2">
|
||||
<Select
|
||||
v-model="getFnMode"
|
||||
:options="getFnOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('templateSearchLab.getFnDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-2xl border border-border-default bg-secondary-background p-6"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-lg font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.keysHeading') }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t('templateSearchLab.keysDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
class="text-xs font-semibold text-text-primary underline-offset-4 hover:underline"
|
||||
:href="docLink('keys')"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ t('templateSearchLab.docLinkLabel') }}
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
{{ t('templateSearchLab.keysHelper') }}
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap items-end gap-3">
|
||||
<div class="grow">
|
||||
<label class="text-xs font-semibold text-muted-foreground">
|
||||
{{ t('templateSearchLab.keysAddLabel') }}
|
||||
</label>
|
||||
<Select
|
||||
v-model="keyToAdd"
|
||||
:options="keyLibrary"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-muted-foreground">
|
||||
{{ t('templateSearchLab.keysWeightLabel') }}
|
||||
</label>
|
||||
<InputNumber
|
||||
v-model.number="newKeyWeight"
|
||||
:min="0.05"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
:label="t('templateSearchLab.keysAddButton')"
|
||||
icon="icon-[lucide--plus-circle]"
|
||||
:disabled="!keyToAdd"
|
||||
@click="addKey"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div
|
||||
v-for="key in fuseKeyEntries"
|
||||
:key="key.id"
|
||||
class="rounded-xl border border-border-muted bg-secondary-background p-4"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-foreground">
|
||||
{{ key.path }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ describeKey(key.path) }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs font-semibold text-danger-100 disabled:text-muted-foreground"
|
||||
:disabled="fuseKeyEntries.length === 1"
|
||||
@click="removeKey(key.id)"
|
||||
>
|
||||
{{ t('templateSearchLab.removeLabel') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3 space-y-2">
|
||||
<InputText v-model="key.path" class="w-full" />
|
||||
<div class="flex items-center gap-3">
|
||||
<Slider
|
||||
v-model.number="key.weight"
|
||||
:min="0.05"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model.number="key.weight"
|
||||
:min="0.05"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside
|
||||
class="flex flex-col gap-4 rounded-2xl border border-border-default bg-secondary-background p-4"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-foreground">
|
||||
{{ t('templateSearchLab.dialogPreviewTitle') }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('templateSearchLab.dialogPreviewSubtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="min-h-[70vh] flex-1 overflow-hidden rounded-xl border border-border-default bg-base-background"
|
||||
>
|
||||
<WorkflowTemplateSelectorDialog :on-close="noop" />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import Fuse from 'fuse.js'
|
||||
import type { RangeTuple } from 'fuse.js'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Select from 'primevue/select'
|
||||
import Slider from 'primevue/slider'
|
||||
import Tag from 'primevue/tag'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, onMounted, provide, reactive, ref } from 'vue'
|
||||
|
||||
import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import {
|
||||
DEFAULT_TEMPLATE_FUSE_CONFIG,
|
||||
TEMPLATE_FUSE_SETTINGS_KEY,
|
||||
buildTemplateFuseOptions
|
||||
} from '@/platform/workflow/templates/utils/templateFuseOptions'
|
||||
import type {
|
||||
TemplateFuseConfig,
|
||||
TemplateFuseGetMode,
|
||||
TemplateFuseKeyConfig,
|
||||
TemplateFuseOptionState,
|
||||
TemplateFuseSortMode
|
||||
} from '@/platform/workflow/templates/utils/templateFuseOptions'
|
||||
import { TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY } from '@/platform/workflow/templates/utils/templateSearchLabInjection'
|
||||
|
||||
interface TemplateSearchRecord extends TemplateInfo {
|
||||
localizedTitle?: string
|
||||
localizedDescription?: string
|
||||
tags?: string[]
|
||||
models?: string[]
|
||||
sourceModule?: string
|
||||
searchableText?: string
|
||||
}
|
||||
|
||||
interface HighlightChunk {
|
||||
text: string
|
||||
isHit: boolean
|
||||
}
|
||||
|
||||
interface FuseKeyEntry {
|
||||
id: string
|
||||
path: string
|
||||
weight: number
|
||||
}
|
||||
|
||||
interface OptionDefinition {
|
||||
key: keyof DesignerOptions | 'sort' | 'get'
|
||||
label: string
|
||||
description: string
|
||||
example: string
|
||||
anchor: string
|
||||
type: 'boolean' | 'number' | 'sort' | 'get'
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}
|
||||
|
||||
interface OptionGroup {
|
||||
key: string
|
||||
title: string
|
||||
description: string
|
||||
options: OptionDefinition[]
|
||||
}
|
||||
|
||||
type TemplateFuseToggleKey =
|
||||
| 'isCaseSensitive'
|
||||
| 'ignoreDiacritics'
|
||||
| 'includeScore'
|
||||
| 'includeMatches'
|
||||
| 'shouldSort'
|
||||
| 'findAllMatches'
|
||||
| 'ignoreLocation'
|
||||
| 'useExtendedSearch'
|
||||
| 'ignoreFieldNorm'
|
||||
|
||||
type TemplateFuseNumberKey =
|
||||
| 'minMatchCharLength'
|
||||
| 'location'
|
||||
| 'threshold'
|
||||
| 'distance'
|
||||
| 'fieldNormWeight'
|
||||
|
||||
type DesignerOptions = TemplateFuseOptionState
|
||||
type DesignerToggleOptions = Pick<
|
||||
TemplateFuseOptionState,
|
||||
TemplateFuseToggleKey
|
||||
>
|
||||
type DesignerNumericOptions = Pick<
|
||||
TemplateFuseOptionState,
|
||||
TemplateFuseNumberKey
|
||||
>
|
||||
|
||||
type SortMode = TemplateFuseSortMode
|
||||
type GetFnMode = TemplateFuseGetMode
|
||||
|
||||
const DOCS_BASE_URL = 'https://www.fusejs.io/api/options.html'
|
||||
|
||||
let keyIdCounter = 0
|
||||
const createKeyId = () => `fuse-key-${++keyIdCounter}`
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const initialConfig =
|
||||
settingStore.get(TEMPLATE_FUSE_SETTINGS_KEY) ?? DEFAULT_TEMPLATE_FUSE_CONFIG
|
||||
|
||||
const designerOptions = reactive<DesignerOptions>({
|
||||
...initialConfig.options
|
||||
})
|
||||
const fuseKeyEntries = ref<FuseKeyEntry[]>(buildKeyEntries(initialConfig.keys))
|
||||
const sortMode = ref<SortMode>(initialConfig.sortMode)
|
||||
const getFnMode = ref<GetFnMode>(initialConfig.getFnMode)
|
||||
const searchQuery = ref('wan')
|
||||
const sampleQueries = ['wan', 'refiner', 'animate', 'face', 'stylized']
|
||||
const keyToAdd = ref('models')
|
||||
const newKeyWeight = ref(0.15)
|
||||
const copyStatus = ref<'idle' | 'copied' | 'error'>('idle')
|
||||
const noop = () => {}
|
||||
|
||||
const workflowTemplatesStore = useWorkflowTemplatesStore()
|
||||
const { enhancedTemplates, isLoaded } = storeToRefs(workflowTemplatesStore)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isLoaded.value) {
|
||||
await workflowTemplatesStore.loadWorkflowTemplates()
|
||||
}
|
||||
})
|
||||
|
||||
const templateRecords = computed<TemplateSearchRecord[]>(
|
||||
() => enhancedTemplates.value as TemplateSearchRecord[]
|
||||
)
|
||||
|
||||
const normalizedQuery = computed(() => searchQuery.value.trim().toLowerCase())
|
||||
|
||||
provide(TEMPLATE_SEARCH_QUERY_OVERRIDE_KEY, searchQuery)
|
||||
|
||||
const currentConfig = computed<TemplateFuseConfig>(() => ({
|
||||
options: { ...designerOptions },
|
||||
keys: fuseKeyEntries.value
|
||||
.filter((entry) => entry.path.trim().length)
|
||||
.map((entry) => ({ path: entry.path.trim(), weight: entry.weight })),
|
||||
sortMode: sortMode.value,
|
||||
getFnMode: getFnMode.value
|
||||
}))
|
||||
|
||||
watchDebounced(
|
||||
() => currentConfig.value,
|
||||
(config) => {
|
||||
void settingStore.set(TEMPLATE_FUSE_SETTINGS_KEY, config)
|
||||
},
|
||||
{ debounce: 250, deep: true }
|
||||
)
|
||||
|
||||
const fuseOptions = computed(() =>
|
||||
buildTemplateFuseOptions<TemplateSearchRecord>({
|
||||
config: currentConfig.value,
|
||||
query: normalizedQuery.value
|
||||
})
|
||||
)
|
||||
|
||||
const previewResults = computed(() => {
|
||||
const templates = templateRecords.value
|
||||
if (!templates.length || !normalizedQuery.value.length) {
|
||||
return []
|
||||
}
|
||||
const fuse = new Fuse(templates, fuseOptions.value)
|
||||
return fuse.search(normalizedQuery.value, { limit: 30 })
|
||||
})
|
||||
|
||||
const previewSummary = computed(() => {
|
||||
if (!searchQuery.value.trim().length) {
|
||||
return t('templateSearchLab.previewSummaryIdle')
|
||||
}
|
||||
if (!isLoaded.value) {
|
||||
return t('templateSearchLab.loadingTemplates')
|
||||
}
|
||||
if (previewResults.value.length === 0) {
|
||||
return t('templateSearchLab.previewSummaryEmpty')
|
||||
}
|
||||
return t('templateSearchLab.previewSummaryActive', {
|
||||
count: previewResults.value.length,
|
||||
total: templateRecords.value.length
|
||||
})
|
||||
})
|
||||
|
||||
const shareableConfig = computed(() =>
|
||||
JSON.stringify(currentConfig.value, null, 2)
|
||||
)
|
||||
|
||||
const resourceLinks = computed(() => [
|
||||
{
|
||||
href: docLink(''),
|
||||
label: t('templateSearchLab.links.apiDocs'),
|
||||
icon: 'icon-[lucide--book-open]'
|
||||
},
|
||||
{
|
||||
href: 'https://www.fusejs.io/concepts/scoring-theory.html',
|
||||
label: t('templateSearchLab.links.scoringTheory'),
|
||||
icon: 'icon-[lucide--line-chart]'
|
||||
},
|
||||
{
|
||||
href: 'https://www.fusejs.io/examples.html#extended-search',
|
||||
label: t('templateSearchLab.links.extendedSearch'),
|
||||
icon: 'icon-[lucide--filter]'
|
||||
}
|
||||
])
|
||||
|
||||
const deepDiveLinks = computed(() => resourceLinks.value.slice(1))
|
||||
|
||||
const sortOptions = computed(() => [
|
||||
{ value: 'score', label: t('templateSearchLab.sortModes.score') },
|
||||
{ value: 'exact', label: t('templateSearchLab.sortModes.exact') },
|
||||
{ value: 'prefix', label: t('templateSearchLab.sortModes.prefix') }
|
||||
])
|
||||
|
||||
const getFnOptions = computed(() => [
|
||||
{ value: 'default', label: t('templateSearchLab.getFnModes.default') },
|
||||
{ value: 'flatten', label: t('templateSearchLab.getFnModes.flatten') }
|
||||
])
|
||||
|
||||
const keyLibrary = computed(() => [
|
||||
{ value: 'name', label: t('templateSearchLab.keysOptions.name') },
|
||||
{ value: 'title', label: t('templateSearchLab.keysOptions.title') },
|
||||
{
|
||||
value: 'description',
|
||||
label: t('templateSearchLab.keysOptions.description')
|
||||
},
|
||||
{ value: 'tags', label: t('templateSearchLab.keysOptions.tags') },
|
||||
{ value: 'models', label: t('templateSearchLab.keysOptions.models') },
|
||||
{ value: 'useCase', label: t('templateSearchLab.keysOptions.useCase') },
|
||||
{
|
||||
value: 'sourceModule',
|
||||
label: t('templateSearchLab.keysOptions.sourceModule')
|
||||
}
|
||||
])
|
||||
|
||||
const optionGroups = computed<OptionGroup[]>(() => [
|
||||
{
|
||||
key: 'basic',
|
||||
title: t('templateSearchLab.optionGroups.basic.title'),
|
||||
description: t('templateSearchLab.optionGroups.basic.description'),
|
||||
options: [
|
||||
createToggleOption('isCaseSensitive', 'iscasesensitive'),
|
||||
createToggleOption('ignoreDiacritics', 'ignorediacritics'),
|
||||
createToggleOption('includeScore', 'includescore'),
|
||||
createToggleOption('includeMatches', 'includematches'),
|
||||
createNumericOption('minMatchCharLength', 'minmatchcharlength', 1, 10, 1),
|
||||
createToggleOption('shouldSort', 'shouldsort'),
|
||||
createToggleOption('findAllMatches', 'findallmatches')
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'fuzzy',
|
||||
title: t('templateSearchLab.optionGroups.fuzzy.title'),
|
||||
description: t('templateSearchLab.optionGroups.fuzzy.description'),
|
||||
options: [
|
||||
createNumericOption('location', 'location', 0, 500, 5),
|
||||
createNumericOption('threshold', 'threshold', 0, 1, 0.01),
|
||||
createNumericOption('distance', 'distance', 0, 1000, 10),
|
||||
createToggleOption('ignoreLocation', 'ignorelocation')
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'advanced',
|
||||
title: t('templateSearchLab.optionGroups.advanced.title'),
|
||||
description: t('templateSearchLab.optionGroups.advanced.description'),
|
||||
options: [
|
||||
createToggleOption('useExtendedSearch', 'useextendedsearch'),
|
||||
{
|
||||
key: 'sort',
|
||||
label: t('templateSearchLab.sortLabel'),
|
||||
description: t('templateSearchLab.sortDescription'),
|
||||
example: t('templateSearchLab.sortExample'),
|
||||
anchor: 'sortFn',
|
||||
type: 'sort'
|
||||
},
|
||||
{
|
||||
key: 'get',
|
||||
label: t('templateSearchLab.getFnLabel'),
|
||||
description: t('templateSearchLab.getFnDescription'),
|
||||
example: t('templateSearchLab.getFnExample'),
|
||||
anchor: 'getFn',
|
||||
type: 'get'
|
||||
},
|
||||
createToggleOption('ignoreFieldNorm', 'ignorefieldnorm'),
|
||||
createNumericOption('fieldNormWeight', 'fieldnormweight', 0, 2, 0.1)
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
function createToggleOption(
|
||||
key: keyof DesignerToggleOptions,
|
||||
anchor: string
|
||||
): OptionDefinition {
|
||||
return {
|
||||
key,
|
||||
anchor,
|
||||
label: key,
|
||||
description: t(`templateSearchLab.options.${String(key)}.description`),
|
||||
example: t(`templateSearchLab.options.${String(key)}.example`),
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
function createNumericOption(
|
||||
key: keyof DesignerNumericOptions,
|
||||
anchor: string,
|
||||
min: number,
|
||||
max: number,
|
||||
step: number
|
||||
): OptionDefinition {
|
||||
return {
|
||||
key,
|
||||
anchor,
|
||||
label: key,
|
||||
description: t(`templateSearchLab.options.${String(key)}.description`),
|
||||
example: t(`templateSearchLab.options.${String(key)}.example`),
|
||||
type: 'number',
|
||||
min,
|
||||
max,
|
||||
step
|
||||
}
|
||||
}
|
||||
|
||||
function buildHighlightChunks(
|
||||
text: string,
|
||||
indices: readonly RangeTuple[]
|
||||
): HighlightChunk[] {
|
||||
if (!indices.length) {
|
||||
return [{ text, isHit: false }]
|
||||
}
|
||||
const chunks: HighlightChunk[] = []
|
||||
let lastIndex = 0
|
||||
indices.forEach(([start, end]) => {
|
||||
if (start > lastIndex) {
|
||||
chunks.push({ text: text.slice(lastIndex, start), isHit: false })
|
||||
}
|
||||
chunks.push({ text: text.slice(start, end + 1), isHit: true })
|
||||
lastIndex = end + 1
|
||||
})
|
||||
if (lastIndex < text.length) {
|
||||
chunks.push({ text: text.slice(lastIndex), isHit: false })
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
function formatScore(score: number) {
|
||||
return score.toFixed(3)
|
||||
}
|
||||
|
||||
function formatTemplateTitle(template: TemplateSearchRecord) {
|
||||
return template.title || template.localizedTitle || template.name
|
||||
}
|
||||
|
||||
function formatTemplateMeta(template: TemplateSearchRecord) {
|
||||
const runsOn = template.openSource === false ? 'External API' : 'ComfyUI'
|
||||
const models = template.models?.slice(0, 2).join(', ')
|
||||
return [runsOn, models].filter(Boolean).join(' • ')
|
||||
}
|
||||
|
||||
function formatNonStringMatch(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function docLink(anchor: string) {
|
||||
return anchor ? `${DOCS_BASE_URL}#${anchor}` : DOCS_BASE_URL
|
||||
}
|
||||
|
||||
function describeKey(path: string) {
|
||||
switch (path) {
|
||||
case 'name':
|
||||
return t('templateSearchLab.keys.nameDescription')
|
||||
case 'title':
|
||||
return t('templateSearchLab.keys.titleDescription')
|
||||
case 'description':
|
||||
return t('templateSearchLab.keys.descriptionDescription')
|
||||
case 'tags':
|
||||
return t('templateSearchLab.keys.tagsDescription')
|
||||
case 'models':
|
||||
return t('templateSearchLab.keys.modelsDescription')
|
||||
default:
|
||||
return t('templateSearchLab.keys.customDescription', { field: path })
|
||||
}
|
||||
}
|
||||
|
||||
function buildKeyEntries(keys: TemplateFuseKeyConfig[]): FuseKeyEntry[] {
|
||||
if (!keys.length) {
|
||||
return DEFAULT_TEMPLATE_FUSE_CONFIG.keys.map((key) => createKeyEntry(key))
|
||||
}
|
||||
return keys.map((key) => createKeyEntry(key))
|
||||
}
|
||||
|
||||
function createKeyEntry(config: TemplateFuseKeyConfig): FuseKeyEntry {
|
||||
return {
|
||||
id: createKeyId(),
|
||||
path: config.path,
|
||||
weight: config.weight
|
||||
}
|
||||
}
|
||||
|
||||
function addKey() {
|
||||
if (!keyToAdd.value) {
|
||||
return
|
||||
}
|
||||
fuseKeyEntries.value.push(
|
||||
createKeyEntry({ path: keyToAdd.value, weight: newKeyWeight.value })
|
||||
)
|
||||
}
|
||||
|
||||
function removeKey(id: string) {
|
||||
if (fuseKeyEntries.value.length === 1) {
|
||||
return
|
||||
}
|
||||
fuseKeyEntries.value = fuseKeyEntries.value.filter((entry) => entry.id !== id)
|
||||
}
|
||||
|
||||
async function copyOptions() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareableConfig.value)
|
||||
copyStatus.value = 'copied'
|
||||
window.setTimeout(() => {
|
||||
copyStatus.value = 'idle'
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
copyStatus.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
function resetLab() {
|
||||
applyConfig(DEFAULT_TEMPLATE_FUSE_CONFIG)
|
||||
searchQuery.value = 'wan'
|
||||
}
|
||||
|
||||
function applyConfig(config: TemplateFuseConfig) {
|
||||
Object.assign(designerOptions, config.options)
|
||||
fuseKeyEntries.value = buildKeyEntries(config.keys)
|
||||
sortMode.value = config.sortMode
|
||||
getFnMode.value = config.getFnMode
|
||||
}
|
||||
</script>
|
||||
@@ -549,9 +549,8 @@ export function useConflictDetection() {
|
||||
async function initializeConflictDetection(): Promise<void> {
|
||||
try {
|
||||
// Check if manager is new Manager before proceeding
|
||||
const { useManagerState } = await import(
|
||||
'@/workbench/extensions/manager/composables/useManagerState'
|
||||
)
|
||||
const { useManagerState } =
|
||||
await import('@/workbench/extensions/manager/composables/useManagerState')
|
||||
const managerState = useManagerState()
|
||||
|
||||
if (!managerState.isNewManagerUI.value) {
|
||||
|
||||
@@ -61,13 +61,6 @@ describe('EssentialsPanel', () => {
|
||||
|
||||
const shortcutsList = wrapper.findComponent(ShortcutsList)
|
||||
expect(shortcutsList.exists()).toBe(true)
|
||||
|
||||
// Should pass only essentials commands
|
||||
const commands = shortcutsList.props('commands')
|
||||
expect(commands).toHaveLength(3)
|
||||
commands.forEach((cmd: ComfyCommandImpl) => {
|
||||
expect(cmd.category).toBe('essentials')
|
||||
})
|
||||
})
|
||||
|
||||
it('should categorize commands into subcategories', () => {
|
||||
@@ -18,9 +18,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
describe('initial state loading', () => {
|
||||
it('should load empty state when localStorage is empty', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
@@ -44,9 +43,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
|
||||
// Need to import the module after localStorage is set
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { acknowledgmentState } = useConflictAcknowledgment()
|
||||
|
||||
expect(acknowledgmentState.value).toEqual({
|
||||
@@ -60,9 +58,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
describe('dismissal functions', () => {
|
||||
it('should mark conflicts as seen with unified function', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { markConflictsAsSeen, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -73,9 +70,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
|
||||
it('should dismiss red dot notification', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { dismissRedDotNotification, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -86,9 +82,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
|
||||
it('should dismiss warning banner', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { dismissWarningBanner, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -99,9 +94,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
|
||||
it('should mark all conflicts as seen', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { markConflictsAsSeen, acknowledgmentState } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -117,9 +111,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
it('should calculate shouldShowConflictModal correctly', async () => {
|
||||
// Need fresh module import to ensure clean state
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { shouldShowConflictModal, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -131,9 +124,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
|
||||
it('should calculate shouldShowRedDot correctly based on conflicts', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { shouldShowRedDot, dismissRedDotNotification } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -146,9 +138,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
|
||||
it('should calculate shouldShowManagerBanner correctly', async () => {
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { shouldShowManagerBanner, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
@@ -164,9 +155,8 @@ describe('useConflictAcknowledgment', () => {
|
||||
it('should persist to localStorage automatically', async () => {
|
||||
// Need fresh module import to ensure clean state
|
||||
vi.resetModules()
|
||||
const { useConflictAcknowledgment } = await import(
|
||||
'@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
)
|
||||
const { useConflictAcknowledgment } =
|
||||
await import('@/workbench/extensions/manager/composables/useConflictAcknowledgment')
|
||||
const { markConflictsAsSeen, dismissWarningBanner } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
|
||||
@@ -153,9 +153,8 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const { useMinimap } = await import(
|
||||
'@/renderer/extensions/minimap/composables/useMinimap'
|
||||
)
|
||||
const { useMinimap } =
|
||||
await import('@/renderer/extensions/minimap/composables/useMinimap')
|
||||
const { api } = await import('@/scripts/api')
|
||||
|
||||
describe('useMinimap', () => {
|
||||
|
||||
@@ -29,9 +29,8 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
const { useTemplateFiltering } = await import(
|
||||
'@/composables/useTemplateFiltering'
|
||||
)
|
||||
const { useTemplateFiltering } =
|
||||
await import('@/composables/useTemplateFiltering')
|
||||
|
||||
describe('useTemplateFiltering', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -103,9 +103,8 @@ describe('useMinimapRenderer', () => {
|
||||
})
|
||||
|
||||
it('should only render when redraw is needed', async () => {
|
||||
const { renderMinimapToCanvas } = await import(
|
||||
'@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
)
|
||||
const { renderMinimapToCanvas } =
|
||||
await import('@/renderer/extensions/minimap/minimapCanvasRenderer')
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
|
||||
@@ -66,9 +66,8 @@ describe('useMinimapViewport', () => {
|
||||
})
|
||||
|
||||
it('should calculate graph bounds from nodes', async () => {
|
||||
const { calculateNodeBounds, enforceMinimumBounds } = await import(
|
||||
'@/renderer/core/spatial/boundsCalculator'
|
||||
)
|
||||
const { calculateNodeBounds, enforceMinimumBounds } =
|
||||
await import('@/renderer/core/spatial/boundsCalculator')
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue({
|
||||
minX: 100,
|
||||
@@ -93,9 +92,8 @@ describe('useMinimapViewport', () => {
|
||||
})
|
||||
|
||||
it('should handle empty graph', async () => {
|
||||
const { calculateNodeBounds } = await import(
|
||||
'@/renderer/core/spatial/boundsCalculator'
|
||||
)
|
||||
const { calculateNodeBounds } =
|
||||
await import('@/renderer/core/spatial/boundsCalculator')
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue(null)
|
||||
|
||||
|
||||
@@ -19,12 +19,10 @@ vi.mock('@/scripts/api', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const { useWorkflowThumbnail } = await import(
|
||||
'@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||
)
|
||||
const { createGraphThumbnail } = await import(
|
||||
'@/renderer/core/thumbnail/graphThumbnailRenderer'
|
||||
)
|
||||
const { useWorkflowThumbnail } =
|
||||
await import('@/renderer/core/thumbnail/useWorkflowThumbnail')
|
||||
const { createGraphThumbnail } =
|
||||
await import('@/renderer/core/thumbnail/graphThumbnailRenderer')
|
||||
const { api } = await import('@/scripts/api')
|
||||
|
||||
describe('useWorkflowThumbnail', () => {
|
||||
|
||||
@@ -62,9 +62,8 @@ describe('useReleaseStore', () => {
|
||||
}
|
||||
|
||||
// Setup mock implementations
|
||||
const { useReleaseService } = await import(
|
||||
'@/platform/updates/common/releaseService'
|
||||
)
|
||||
const { useReleaseService } =
|
||||
await import('@/platform/updates/common/releaseService')
|
||||
const { useSettingStore } = await import('@/platform/settings/settingStore')
|
||||
const { useSystemStatsStore } = await import('@/stores/systemStatsStore')
|
||||
const { isElectron } = await import('@/utils/envUtil')
|
||||
|
||||
@@ -287,9 +287,8 @@ describe('versionUtil', () => {
|
||||
vi.resetModules()
|
||||
|
||||
// Import fresh module
|
||||
const versionUtil = await import(
|
||||
'@/workbench/extensions/manager/utils/versionUtil'
|
||||
)
|
||||
const versionUtil =
|
||||
await import('@/workbench/extensions/manager/utils/versionUtil')
|
||||
|
||||
const version = versionUtil.getFrontendVersion()
|
||||
expect(version).toBe('2.0.0')
|
||||
@@ -322,9 +321,8 @@ describe('versionUtil', () => {
|
||||
vi.resetModules()
|
||||
|
||||
// Import fresh module
|
||||
const versionUtil = await import(
|
||||
'@/workbench/extensions/manager/utils/versionUtil'
|
||||
)
|
||||
const versionUtil =
|
||||
await import('@/workbench/extensions/manager/utils/versionUtil')
|
||||
|
||||
const version = versionUtil.getFrontendVersion()
|
||||
expect(version).toBeUndefined()
|
||||
|
||||
Reference in New Issue
Block a user