mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat(assets): add ModelInfoPanel for asset browser right panel
Amp-Thread-ID: https://ampcode.com/threads/T-019bc42f-b9b7-71de-9d8f-6584610ab21e Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -2385,6 +2385,21 @@
|
||||
"assetCard": "{name} - {type} asset",
|
||||
"loadingAsset": "Loading asset"
|
||||
},
|
||||
"modelInfo": {
|
||||
"title": "Model Info",
|
||||
"basicInfo": "Basic Info",
|
||||
"displayName": "Display Name",
|
||||
"fileName": "File Name",
|
||||
"source": "Source",
|
||||
"viewOnSource": "View on {source}",
|
||||
"modelTagging": "Model Tagging",
|
||||
"modelType": "Model Type",
|
||||
"compatibleBaseModels": "Compatible Base Models",
|
||||
"additionalTags": "Additional Tags",
|
||||
"modelDescription": "Model Description",
|
||||
"triggerPhrases": "Trigger Phrases",
|
||||
"description": "Description"
|
||||
},
|
||||
"media": {
|
||||
"threeDModelPlaceholder": "3D Model",
|
||||
"audioPlaceholder": "Audio"
|
||||
|
||||
@@ -60,12 +60,16 @@
|
||||
@asset-deleted="refreshAssets"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="selectedAsset" #rightPanel>
|
||||
<ModelInfoPanel :asset="selectedAsset" />
|
||||
</template>
|
||||
</BaseModalLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import { computed, provide } from 'vue'
|
||||
import { computed, provide, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
@@ -74,6 +78,7 @@ import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
|
||||
import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
|
||||
import ModelInfoPanel from '@/platform/assets/components/modelInfo/ModelInfoPanel.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
|
||||
@@ -150,6 +155,8 @@ const {
|
||||
updateFilters
|
||||
} = useAssetBrowser(fetchedAssets)
|
||||
|
||||
const selectedAsset = ref<AssetDisplayItem | null>(null)
|
||||
|
||||
const primaryCategoryTag = computed(() => {
|
||||
const assets = fetchedAssets.value ?? []
|
||||
const tagFromAssets = assets
|
||||
@@ -192,6 +199,7 @@ function handleClose() {
|
||||
}
|
||||
|
||||
function handleAssetSelectAndEmit(asset: AssetDisplayItem) {
|
||||
selectedAsset.value = asset
|
||||
emit('asset-select', asset)
|
||||
// onSelect callback is provided by dialog composable layer
|
||||
// It handles the appropriate transformation (filename extraction or full asset)
|
||||
|
||||
12
src/platform/assets/components/modelInfo/ModelInfoField.vue
Normal file
12
src/platform/assets/components/modelInfo/ModelInfoField.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1 px-4 py-2">
|
||||
<span class="text-xs text-muted-foreground">{{ label }}</span>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
}>()
|
||||
</script>
|
||||
140
src/platform/assets/components/modelInfo/ModelInfoPanel.vue
Normal file
140
src/platform/assets/components/modelInfo/ModelInfoPanel.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col bg-comfy-menu-bg">
|
||||
<div class="flex h-18 items-center border-b border-divider px-4">
|
||||
<h2 class="text-lg font-semibold">
|
||||
{{ $t('assetBrowser.modelInfo.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto scrollbar-custom">
|
||||
<PropertiesAccordionItem>
|
||||
<template #label>
|
||||
<span class="text-xs uppercase">
|
||||
{{ $t('assetBrowser.modelInfo.basicInfo') }}
|
||||
</span>
|
||||
</template>
|
||||
<ModelInfoField :label="$t('assetBrowser.modelInfo.displayName')">
|
||||
<span class="text-sm">{{ displayName }}</span>
|
||||
</ModelInfoField>
|
||||
<ModelInfoField :label="$t('assetBrowser.modelInfo.fileName')">
|
||||
<span class="text-sm">{{ asset.name }}</span>
|
||||
</ModelInfoField>
|
||||
<ModelInfoField
|
||||
v-if="sourceUrl"
|
||||
:label="$t('assetBrowser.modelInfo.source')"
|
||||
>
|
||||
<a
|
||||
:href="sourceUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-link hover:underline"
|
||||
>
|
||||
{{
|
||||
$t('assetBrowser.modelInfo.viewOnSource', { source: sourceName })
|
||||
}}
|
||||
</a>
|
||||
</ModelInfoField>
|
||||
</PropertiesAccordionItem>
|
||||
|
||||
<PropertiesAccordionItem>
|
||||
<template #label>
|
||||
<span class="text-xs uppercase">
|
||||
{{ $t('assetBrowser.modelInfo.modelTagging') }}
|
||||
</span>
|
||||
</template>
|
||||
<ModelInfoField
|
||||
v-if="modelType"
|
||||
:label="$t('assetBrowser.modelInfo.modelType')"
|
||||
>
|
||||
<span class="text-sm">{{ modelType }}</span>
|
||||
</ModelInfoField>
|
||||
<ModelInfoField
|
||||
v-if="baseModel"
|
||||
:label="$t('assetBrowser.modelInfo.compatibleBaseModels')"
|
||||
>
|
||||
<span class="text-sm">{{ baseModel }}</span>
|
||||
</ModelInfoField>
|
||||
<ModelInfoField
|
||||
v-if="additionalTags.length > 0"
|
||||
:label="$t('assetBrowser.modelInfo.additionalTags')"
|
||||
>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="tag in additionalTags"
|
||||
:key="tag"
|
||||
class="rounded bg-surface-container px-2 py-0.5 text-xs"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</ModelInfoField>
|
||||
</PropertiesAccordionItem>
|
||||
|
||||
<PropertiesAccordionItem>
|
||||
<template #label>
|
||||
<span class="text-xs uppercase">
|
||||
{{ $t('assetBrowser.modelInfo.modelDescription') }}
|
||||
</span>
|
||||
</template>
|
||||
<ModelInfoField
|
||||
v-if="triggerPhrases.length > 0"
|
||||
:label="$t('assetBrowser.modelInfo.triggerPhrases')"
|
||||
>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="phrase in triggerPhrases"
|
||||
:key="phrase"
|
||||
class="rounded bg-surface-container px-2 py-0.5 text-xs"
|
||||
>
|
||||
{{ phrase }}
|
||||
</span>
|
||||
</div>
|
||||
</ModelInfoField>
|
||||
<ModelInfoField
|
||||
v-if="description"
|
||||
:label="$t('assetBrowser.modelInfo.description')"
|
||||
>
|
||||
<p class="text-sm whitespace-pre-wrap">{{ description }}</p>
|
||||
</ModelInfoField>
|
||||
</PropertiesAccordionItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
|
||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import {
|
||||
getAssetBaseModel,
|
||||
getAssetDescription,
|
||||
getAssetDisplayName,
|
||||
getAssetSourceUrl,
|
||||
getAssetTags,
|
||||
getAssetTriggerPhrases,
|
||||
getSourceName
|
||||
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||
|
||||
import ModelInfoField from './ModelInfoField.vue'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetDisplayItem
|
||||
}>()
|
||||
|
||||
const displayName = computed(() => getAssetDisplayName(asset))
|
||||
const sourceUrl = computed(() => getAssetSourceUrl(asset))
|
||||
const sourceName = computed(() =>
|
||||
sourceUrl.value ? getSourceName(sourceUrl.value) : ''
|
||||
)
|
||||
const baseModel = computed(() => getAssetBaseModel(asset))
|
||||
const description = computed(() => getAssetDescription(asset))
|
||||
const triggerPhrases = computed(() => getAssetTriggerPhrases(asset))
|
||||
const additionalTags = computed(() => getAssetTags(asset))
|
||||
|
||||
const modelType = computed(() => {
|
||||
const typeTag = asset.tags.find((tag) => tag !== 'models')
|
||||
if (!typeTag) return null
|
||||
return typeTag.includes('/') ? typeTag.split('/').pop() : typeTag
|
||||
})
|
||||
</script>
|
||||
@@ -25,3 +25,63 @@ export function getAssetBaseModel(asset: AssetItem): string | null {
|
||||
? asset.user_metadata.base_model
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name for an asset, falling back to filename
|
||||
* @param asset - The asset to get display name from
|
||||
* @returns The display name or filename
|
||||
*/
|
||||
export function getAssetDisplayName(asset: AssetItem): string {
|
||||
return typeof asset.user_metadata?.display_name === 'string'
|
||||
? asset.user_metadata.display_name
|
||||
: asset.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extracts source URL from asset metadata
|
||||
* @param asset - The asset to extract source URL from
|
||||
* @returns The source URL or null if not present
|
||||
*/
|
||||
export function getAssetSourceUrl(asset: AssetItem): string | null {
|
||||
return typeof asset.user_metadata?.source_url === 'string'
|
||||
? asset.user_metadata.source_url
|
||||
: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts trigger phrases from asset metadata
|
||||
* @param asset - The asset to extract trigger phrases from
|
||||
* @returns Array of trigger phrases
|
||||
*/
|
||||
export function getAssetTriggerPhrases(asset: AssetItem): string[] {
|
||||
const phrases = asset.user_metadata?.trigger_phrases
|
||||
if (Array.isArray(phrases)) {
|
||||
return phrases.filter((p): p is string => typeof p === 'string')
|
||||
}
|
||||
if (typeof phrases === 'string') return [phrases]
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts additional tags from asset user_metadata
|
||||
* @param asset - The asset to extract tags from
|
||||
* @returns Array of user-defined tags
|
||||
*/
|
||||
export function getAssetTags(asset: AssetItem): string[] {
|
||||
const tags = asset.user_metadata?.tags
|
||||
if (Array.isArray(tags)) {
|
||||
return tags.filter((t): t is string => typeof t === 'string')
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the source name from a URL
|
||||
* @param url - The source URL
|
||||
* @returns Human-readable source name
|
||||
*/
|
||||
export function getSourceName(url: string): string {
|
||||
if (url.includes('civitai.com')) return 'Civitai'
|
||||
if (url.includes('huggingface.co')) return 'Hugging Face'
|
||||
return 'Source'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user