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:
Alexander Brown
2026-01-15 16:27:45 -08:00
parent 30907f99f1
commit e838f0ae62
5 changed files with 236 additions and 1 deletions

View File

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

View 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>

View 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>