mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
feat(ModelInfoPanel): use TagsInput for Additional Tags
This commit is contained in:
@@ -2409,6 +2409,8 @@
|
|||||||
"addBaseModel": "Add base model...",
|
"addBaseModel": "Add base model...",
|
||||||
"baseModelUnknown": "Base model unknown",
|
"baseModelUnknown": "Base model unknown",
|
||||||
"additionalTags": "Additional Tags",
|
"additionalTags": "Additional Tags",
|
||||||
|
"addTag": "Add tag...",
|
||||||
|
"noAdditionalTags": "No additional tags",
|
||||||
"modelDescription": "Model Description",
|
"modelDescription": "Model Description",
|
||||||
"triggerPhrases": "Trigger Phrases",
|
"triggerPhrases": "Trigger Phrases",
|
||||||
"description": "Description"
|
"description": "Description"
|
||||||
|
|||||||
@@ -77,19 +77,25 @@
|
|||||||
/>
|
/>
|
||||||
</TagsInput>
|
</TagsInput>
|
||||||
</ModelInfoField>
|
</ModelInfoField>
|
||||||
<ModelInfoField
|
<ModelInfoField :label="$t('assetBrowser.modelInfo.additionalTags')">
|
||||||
v-if="additionalTags.length > 0"
|
<TagsInput
|
||||||
:label="$t('assetBrowser.modelInfo.additionalTags')"
|
v-slot="{ isEmpty }"
|
||||||
>
|
v-model="additionalTags"
|
||||||
<div class="flex flex-wrap gap-1">
|
:disabled="isImmutable"
|
||||||
<span
|
>
|
||||||
v-for="tag in additionalTags"
|
<TagsInputItem v-for="tag in additionalTags" :key="tag" :value="tag">
|
||||||
:key="tag"
|
<TagsInputItemText />
|
||||||
class="rounded px-2 py-0.5 text-xs"
|
<TagsInputItemDelete />
|
||||||
>
|
</TagsInputItem>
|
||||||
{{ tag }}
|
<TagsInputInput
|
||||||
</span>
|
:is-empty="isEmpty"
|
||||||
</div>
|
:placeholder="
|
||||||
|
isImmutable
|
||||||
|
? $t('assetBrowser.modelInfo.noAdditionalTags')
|
||||||
|
: $t('assetBrowser.modelInfo.addTag')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</TagsInput>
|
||||||
</ModelInfoField>
|
</ModelInfoField>
|
||||||
</PropertiesAccordionItem>
|
</PropertiesAccordionItem>
|
||||||
|
|
||||||
@@ -134,11 +140,11 @@ import TagsInputItemDelete from '@/components/ui/tags-input/TagsInputItemDelete.
|
|||||||
import TagsInputItemText from '@/components/ui/tags-input/TagsInputItemText.vue'
|
import TagsInputItemText from '@/components/ui/tags-input/TagsInputItemText.vue'
|
||||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||||
import {
|
import {
|
||||||
|
getAssetAdditionalTags,
|
||||||
getAssetBaseModels,
|
getAssetBaseModels,
|
||||||
getAssetDescription,
|
getAssetDescription,
|
||||||
getAssetDisplayName,
|
getAssetDisplayName,
|
||||||
getAssetSourceUrl,
|
getAssetSourceUrl,
|
||||||
getAssetTags,
|
|
||||||
getAssetTriggerPhrases,
|
getAssetTriggerPhrases,
|
||||||
getSourceName
|
getSourceName
|
||||||
} from '@/platform/assets/utils/assetMetadataUtils'
|
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||||
@@ -157,15 +163,16 @@ const sourceName = computed(() =>
|
|||||||
sourceUrl.value ? getSourceName(sourceUrl.value) : ''
|
sourceUrl.value ? getSourceName(sourceUrl.value) : ''
|
||||||
)
|
)
|
||||||
const baseModels = ref<string[]>(getAssetBaseModels(asset))
|
const baseModels = ref<string[]>(getAssetBaseModels(asset))
|
||||||
|
const additionalTags = ref<string[]>(getAssetAdditionalTags(asset))
|
||||||
watch(
|
watch(
|
||||||
() => asset,
|
() => asset,
|
||||||
() => {
|
() => {
|
||||||
baseModels.value = getAssetBaseModels(asset)
|
baseModels.value = getAssetBaseModels(asset)
|
||||||
|
additionalTags.value = getAssetAdditionalTags(asset)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const description = computed(() => getAssetDescription(asset))
|
const description = computed(() => getAssetDescription(asset))
|
||||||
const triggerPhrases = computed(() => getAssetTriggerPhrases(asset))
|
const triggerPhrases = computed(() => getAssetTriggerPhrases(asset))
|
||||||
const additionalTags = computed(() => getAssetTags(asset))
|
|
||||||
const isImmutable = computed(() => asset.is_immutable ?? true)
|
const isImmutable = computed(() => asset.is_immutable ?? true)
|
||||||
|
|
||||||
const modelType = computed(() => {
|
const modelType = computed(() => {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||||
import {
|
import {
|
||||||
|
getAssetAdditionalTags,
|
||||||
getAssetBaseModel,
|
getAssetBaseModel,
|
||||||
getAssetDescription,
|
getAssetDescription,
|
||||||
getAssetDisplayName,
|
getAssetDisplayName,
|
||||||
getAssetSourceUrl,
|
getAssetSourceUrl,
|
||||||
getAssetTags,
|
|
||||||
getAssetTriggerPhrases,
|
getAssetTriggerPhrases,
|
||||||
getSourceName
|
getSourceName
|
||||||
} from '@/platform/assets/utils/assetMetadataUtils'
|
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||||
@@ -69,18 +69,18 @@ describe('assetMetadataUtils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('getAssetDisplayName', () => {
|
describe('getAssetDisplayName', () => {
|
||||||
it('should return display_name when present', () => {
|
it('should return name from user_metadata when present', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { display_name: 'My Custom Name' }
|
user_metadata: { name: 'My Custom Name' }
|
||||||
}
|
}
|
||||||
expect(getAssetDisplayName(asset)).toBe('My Custom Name')
|
expect(getAssetDisplayName(asset)).toBe('My Custom Name')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fall back to asset name when display_name is not a string', () => {
|
it('should fall back to asset name when user_metadata.name is not a string', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { display_name: 123 }
|
user_metadata: { name: 123 }
|
||||||
}
|
}
|
||||||
expect(getAssetDisplayName(asset)).toBe('test-model')
|
expect(getAssetDisplayName(asset)).toBe('test-model')
|
||||||
})
|
})
|
||||||
@@ -91,18 +91,28 @@ describe('assetMetadataUtils', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('getAssetSourceUrl', () => {
|
describe('getAssetSourceUrl', () => {
|
||||||
it('should return source_url when present', () => {
|
it('should construct URL from source_arn with civitai format', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { source_url: 'https://civitai.com/models/123' }
|
user_metadata: { source_arn: 'civitai:model:123:version:456' }
|
||||||
}
|
}
|
||||||
expect(getAssetSourceUrl(asset)).toBe('https://civitai.com/models/123')
|
expect(getAssetSourceUrl(asset)).toBe(
|
||||||
|
'https://civitai.com/models/123?modelVersionId=456'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return null when source_url is not a string', () => {
|
it('should return null when source_arn is not a string', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { source_url: 123 }
|
user_metadata: { source_arn: 123 }
|
||||||
|
}
|
||||||
|
expect(getAssetSourceUrl(asset)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null when source_arn format is not recognized', () => {
|
||||||
|
const asset = {
|
||||||
|
...mockAsset,
|
||||||
|
user_metadata: { source_arn: 'unknown:format' }
|
||||||
}
|
}
|
||||||
expect(getAssetSourceUrl(asset)).toBeNull()
|
expect(getAssetSourceUrl(asset)).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -116,7 +126,7 @@ describe('assetMetadataUtils', () => {
|
|||||||
it('should return array of trigger phrases when array present', () => {
|
it('should return array of trigger phrases when array present', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { trigger_phrases: ['phrase1', 'phrase2'] }
|
user_metadata: { trained_words: ['phrase1', 'phrase2'] }
|
||||||
}
|
}
|
||||||
expect(getAssetTriggerPhrases(asset)).toEqual(['phrase1', 'phrase2'])
|
expect(getAssetTriggerPhrases(asset)).toEqual(['phrase1', 'phrase2'])
|
||||||
})
|
})
|
||||||
@@ -124,7 +134,7 @@ describe('assetMetadataUtils', () => {
|
|||||||
it('should wrap single string in array', () => {
|
it('should wrap single string in array', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { trigger_phrases: 'single phrase' }
|
user_metadata: { trained_words: 'single phrase' }
|
||||||
}
|
}
|
||||||
expect(getAssetTriggerPhrases(asset)).toEqual(['single phrase'])
|
expect(getAssetTriggerPhrases(asset)).toEqual(['single phrase'])
|
||||||
})
|
})
|
||||||
@@ -132,7 +142,7 @@ describe('assetMetadataUtils', () => {
|
|||||||
it('should filter non-string values from array', () => {
|
it('should filter non-string values from array', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { trigger_phrases: ['valid', 123, 'also valid', null] }
|
user_metadata: { trained_words: ['valid', 123, 'also valid', null] }
|
||||||
}
|
}
|
||||||
expect(getAssetTriggerPhrases(asset)).toEqual(['valid', 'also valid'])
|
expect(getAssetTriggerPhrases(asset)).toEqual(['valid', 'also valid'])
|
||||||
})
|
})
|
||||||
@@ -142,33 +152,33 @@ describe('assetMetadataUtils', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getAssetTags', () => {
|
describe('getAssetAdditionalTags', () => {
|
||||||
it('should return array of tags when present', () => {
|
it('should return array of tags when present', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { tags: ['tag1', 'tag2'] }
|
user_metadata: { additional_tags: ['tag1', 'tag2'] }
|
||||||
}
|
}
|
||||||
expect(getAssetTags(asset)).toEqual(['tag1', 'tag2'])
|
expect(getAssetAdditionalTags(asset)).toEqual(['tag1', 'tag2'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should filter non-string values from array', () => {
|
it('should filter non-string values from array', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { tags: ['valid', 123, 'also valid'] }
|
user_metadata: { additional_tags: ['valid', 123, 'also valid'] }
|
||||||
}
|
}
|
||||||
expect(getAssetTags(asset)).toEqual(['valid', 'also valid'])
|
expect(getAssetAdditionalTags(asset)).toEqual(['valid', 'also valid'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return empty array when tags is not an array', () => {
|
it('should return empty array when additional_tags is not an array', () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
...mockAsset,
|
...mockAsset,
|
||||||
user_metadata: { tags: 'not an array' }
|
user_metadata: { additional_tags: 'not an array' }
|
||||||
}
|
}
|
||||||
expect(getAssetTags(asset)).toEqual([])
|
expect(getAssetAdditionalTags(asset)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return empty array when no metadata', () => {
|
it('should return empty array when no metadata', () => {
|
||||||
expect(getAssetTags(mockAsset)).toEqual([])
|
expect(getAssetAdditionalTags(mockAsset)).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ export function getAssetTriggerPhrases(asset: AssetItem): string[] {
|
|||||||
* @param asset - The asset to extract tags from
|
* @param asset - The asset to extract tags from
|
||||||
* @returns Array of user-defined tags
|
* @returns Array of user-defined tags
|
||||||
*/
|
*/
|
||||||
export function getAssetTags(asset: AssetItem): string[] {
|
export function getAssetAdditionalTags(asset: AssetItem): string[] {
|
||||||
const tags = asset.user_metadata?.tags
|
const tags = asset.user_metadata?.additional_tags
|
||||||
if (Array.isArray(tags)) {
|
if (Array.isArray(tags)) {
|
||||||
return tags.filter((t): t is string => typeof t === 'string')
|
return tags.filter((t): t is string => typeof t === 'string')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user