feat(assets): add ModelInfoPanel for asset browser right panel (#8090)

## Summary

Adds an editable Model Info Panel to show and modify asset details in
the asset browser.

## Changes

- Add `ModelInfoPanel` component with editable display name,
description, model type, base models, and tags
- Add `updateAssetMetadata` action in `assetsStore` with optimistic
cache updates
- Add shadcn-vue `Select` components with design system styling
- Add utility functions in `assetMetadataUtils` for extracting model
metadata
- Convert `BaseModalLayout` right panel state to `defineModel` pattern
- Add slide-in animation and collapse button for right panel
- Add `class` prop to `PropertiesAccordionItem` for custom styling
- Fix keyboard handling: Escape in TagsInput/TextArea doesn't close
parent modal

## Testing

- Unit tests for `ModelInfoPanel` component
- Unit tests for `assetMetadataUtils` functions

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Alexander Brown
2026-01-21 19:43:56 -08:00
committed by GitHub
parent 8261e1d187
commit 93e7a4f9f9
25 changed files with 1198 additions and 314 deletions

View File

@@ -319,8 +319,8 @@ describe('assetsStore - Refactored (Option A)', () => {
// Verify sorting (newest first - lower index = newer)
for (let i = 1; i < store.historyAssets.length; i++) {
const prevDate = new Date(store.historyAssets[i - 1].created_at)
const currDate = new Date(store.historyAssets[i].created_at)
const prevDate = new Date(store.historyAssets[i - 1].created_at ?? 0)
const currDate = new Date(store.historyAssets[i].created_at ?? 0)
expect(prevDate.getTime()).toBeGreaterThanOrEqual(currDate.getTime())
}
})
@@ -435,8 +435,8 @@ describe('assetsStore - Refactored (Option A)', () => {
// Should still maintain sorting
for (let i = 1; i < store.historyAssets.length; i++) {
const prevDate = new Date(store.historyAssets[i - 1].created_at)
const currDate = new Date(store.historyAssets[i].created_at)
const prevDate = new Date(store.historyAssets[i - 1].created_at ?? 0)
const currDate = new Date(store.historyAssets[i].created_at ?? 0)
expect(prevDate.getTime()).toBeGreaterThanOrEqual(currDate.getTime())
}
})

View File

@@ -78,7 +78,8 @@ function mapHistoryToAssets(historyItems: JobListItem[]): AssetItem[] {
return assetItems.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
new Date(b.created_at ?? 0).getTime() -
new Date(a.created_at ?? 0).getTime()
)
}
@@ -145,9 +146,9 @@ export const useAssetsStore = defineStore('assets', () => {
loadedIds.add(asset.id)
// Find insertion index to maintain sorted order (newest first)
const assetTime = new Date(asset.created_at).getTime()
const assetTime = new Date(asset.created_at ?? 0).getTime()
const insertIndex = allHistoryItems.value.findIndex(
(item) => new Date(item.created_at).getTime() < assetTime
(item) => new Date(item.created_at ?? 0).getTime() < assetTime
)
if (insertIndex === -1) {
@@ -321,6 +322,10 @@ export const useAssetsStore = defineStore('assets', () => {
return modelStateByKey.value.get(key)?.hasMore ?? false
}
function hasAssetKey(key: string): boolean {
return modelStateByKey.value.has(key)
}
/**
* Internal helper to fetch and cache assets with a given key and fetcher.
* Loads first batch immediately, then progressively loads remaining batches.
@@ -419,13 +424,75 @@ export const useAssetsStore = defineStore('assets', () => {
)
}
/**
* Optimistically update an asset in the cache
* @param assetId The asset ID to update
* @param updates Partial asset data to merge
* @param cacheKey Optional cache key to target (nodeType or 'tag:xxx')
*/
function updateAssetInCache(
assetId: string,
updates: Partial<AssetItem>,
cacheKey?: string
) {
const keysToCheck = cacheKey
? [cacheKey]
: Array.from(modelStateByKey.value.keys())
for (const key of keysToCheck) {
const state = modelStateByKey.value.get(key)
if (!state?.assets) continue
const existingAsset = state.assets.get(assetId)
if (existingAsset) {
const updatedAsset = { ...existingAsset, ...updates }
state.assets.set(assetId, updatedAsset)
assetsArrayCache.delete(key)
if (cacheKey) return
}
}
}
/**
* Update asset metadata with optimistic cache update
* @param assetId The asset ID to update
* @param userMetadata The user_metadata to save
* @param cacheKey Optional cache key to target for optimistic update
*/
async function updateAssetMetadata(
assetId: string,
userMetadata: Record<string, unknown>,
cacheKey?: string
) {
updateAssetInCache(assetId, { user_metadata: userMetadata }, cacheKey)
await assetService.updateAsset(assetId, { user_metadata: userMetadata })
}
/**
* Update asset tags with optimistic cache update
* @param assetId The asset ID to update
* @param tags The tags array to save
* @param cacheKey Optional cache key to target for optimistic update
*/
async function updateAssetTags(
assetId: string,
tags: string[],
cacheKey?: string
) {
updateAssetInCache(assetId, { tags }, cacheKey)
await assetService.updateAsset(assetId, { tags })
}
return {
getAssets,
isLoading,
getError,
hasMore,
hasAssetKey,
updateModelsForNodeType,
updateModelsForTag
updateModelsForTag,
updateAssetMetadata,
updateAssetTags
}
}
@@ -435,8 +502,11 @@ export const useAssetsStore = defineStore('assets', () => {
isLoading: () => false,
getError: () => undefined,
hasMore: () => false,
hasAssetKey: () => false,
updateModelsForNodeType: async () => {},
updateModelsForTag: async () => {}
updateModelsForTag: async () => {},
updateAssetMetadata: async () => {},
updateAssetTags: async () => {}
}
}
@@ -445,8 +515,11 @@ export const useAssetsStore = defineStore('assets', () => {
isLoading: isModelLoading,
getError,
hasMore,
hasAssetKey,
updateModelsForNodeType,
updateModelsForTag
updateModelsForTag,
updateAssetMetadata,
updateAssetTags
} = getModelState()
// Watch for completed downloads and refresh model caches
@@ -511,9 +584,12 @@ export const useAssetsStore = defineStore('assets', () => {
isModelLoading,
getError,
hasMore,
hasAssetKey,
// Model assets - actions
updateModelsForNodeType,
updateModelsForTag
updateModelsForTag,
updateAssetMetadata,
updateAssetTags
}
})