feat(ModelInfoPanel): use TagsInput for Additional Tags

This commit is contained in:
Alexander Brown
2026-01-16 13:27:42 -08:00
parent d824d643b0
commit 87173ee2d5
4 changed files with 58 additions and 39 deletions

View File

@@ -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"

View File

@@ -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(() => {

View File

@@ -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([])
}) })
}) })

View File

@@ -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')
} }