Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"preview": "nx preview",
"lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix",
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just helper functions for myself.

"lint:no-cache": "eslint src",
"lint:fix:no-cache": "eslint src --fix",
"knip": "knip --cache",
Expand Down
2 changes: 1 addition & 1 deletion src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const i18n = createI18n({
})

/** Convenience shorthand: i18n.global */
export const { t, te } = i18n.global
export const { t, te, d } = i18n.global

/**
* Safe translation function that returns the fallback message if the key is not found.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/litegraph/src/litegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export { BaseWidget } from './widgets/BaseWidget'

export { LegacyWidget } from './widgets/LegacyWidget'

export { isComboWidget } from './widgets/widgetMap'
export { isComboWidget, isAssetWidget } from './widgets/widgetMap'
// Additional test-specific exports
export { LGraphButton } from './LGraphButton'
export { MovingOutputLink } from './canvas/MovingOutputLink'
Expand Down
16 changes: 16 additions & 0 deletions src/lib/litegraph/src/widgets/AssetWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ export class AssetWidget
this.value = widget.value?.toString() ?? ''
}

override set value(value: IAssetWidget['value']) {
const oldValue = this.value
super.value = value

// Force canvas redraw when value changes to show update immediately
if (oldValue !== value && this.node.graph?.list_of_graphcanvas) {
for (const canvas of this.node.graph.list_of_graphcanvas) {
canvas.setDirty(true)
}
}
}

override get value(): IAssetWidget['value'] {
return super.value
}

override get _displayValue(): string {
return String(this.value) //FIXME: Resolve asset name
}
Expand Down
6 changes: 6 additions & 0 deletions src/lib/litegraph/src/widgets/widgetMap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type {
IAssetWidget,
IBaseWidget,
IComboWidget,
IWidget,
Expand Down Expand Up @@ -132,4 +133,9 @@ export function isComboWidget(widget: IBaseWidget): widget is IComboWidget {
return widget.type === 'combo'
}

/** Type guard: Narrow **from {@link IBaseWidget}** to {@link IAssetWidget}. */
export function isAssetWidget(widget: IBaseWidget): widget is IAssetWidget {
return widget.type === 'asset'
}

// #endregion Type Guards
9 changes: 8 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,13 @@
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Search assets...",
"allModels": "All Models",
"unknown": "Unknown"
"unknown": "Unknown",
"fileFormats": "File formats",
"baseModels": "Base models",
"sortBy": "Sort by",
"sortAZ": "A-Z",
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular"
}
}
7 changes: 4 additions & 3 deletions src/platform/assets/components/AssetBrowserModal.stories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'

import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import {
createMockAssets,
mockAssets
Expand Down Expand Up @@ -56,7 +57,7 @@ export const Default: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: any) => {
const onAssetSelect = (asset: AssetDisplayItem) => {
console.log('Selected asset:', asset)
}
const onClose = () => {
Expand Down Expand Up @@ -96,7 +97,7 @@ export const SingleAssetType: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: any) => {
const onAssetSelect = (asset: AssetDisplayItem) => {
console.log('Selected asset:', asset)
}
const onClose = () => {
Expand Down Expand Up @@ -145,7 +146,7 @@ export const NoLeftPanel: Story = {
render: (args) => ({
components: { AssetBrowserModal },
setup() {
const onAssetSelect = (asset: any) => {
const onAssetSelect = (asset: AssetDisplayItem) => {
console.log('Selected asset:', asset)
}
const onClose = () => {
Expand Down
24 changes: 9 additions & 15 deletions src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
:nav-items="availableCategories"
>
<template #header-icon>
<i-lucide:folder class="size-4" />
<div class="icon-[lucide--folder] size-4" />
</template>
<template #header-title>{{ $t('assetBrowser.browseAssets') }}</template>
</LeftSidePanel>
Expand All @@ -37,7 +37,7 @@
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { computed, provide } from 'vue'

import SearchBox from '@/components/input/SearchBox.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
Expand All @@ -46,6 +46,7 @@ import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { OnCloseKey } from '@/types/widgetTypes'

const props = defineProps<{
nodeType?: string
Expand All @@ -61,35 +62,28 @@ const emit = defineEmits<{
close: []
}>()

// Use AssetBrowser composable for all business logic
provide(OnCloseKey, props.onClose ?? (() => {}))

const {
searchQuery,
selectedCategory,
availableCategories,
contentTitle,
filteredAssets,
selectAsset
selectAssetWithCallback
} = useAssetBrowser(props.assets)

// Dialog controls panel visibility via prop
const shouldShowLeftPanel = computed(() => {
return props.showLeftPanel ?? true
})

// Handle close button - call both the prop callback and emit the event
const handleClose = () => {
props.onClose?.()
emit('close')
}

// Handle asset selection and emit to parent
const handleAssetSelectAndEmit = (asset: AssetDisplayItem) => {
selectAsset(asset) // This logs the selection for dev mode
emit('asset-select', asset) // Emit the full asset object

// Call prop callback if provided
if (props.onSelect) {
props.onSelect(asset.name) // Use asset name as the asset path
}
const handleAssetSelectAndEmit = async (asset: AssetDisplayItem) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: (This does not need to be done in this PR). We can consider moving the selection to the onBeforeUnmount of the modal. One benefit is that right now, we are doing a blocking schema validation and potentially network request on each selection which may cause sluggishness during UI interactions (compared with deferring that to a single time when closing dialog).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look at this in follow up.

emit('asset-select', asset)
await selectAssetWithCallback(asset.id, props.onSelect)
}
</script>
2 changes: 1 addition & 1 deletion src/platform/assets/components/AssetCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
'bg-ivory-100 border border-gray-300 dark-theme:bg-charcoal-400 dark-theme:border-charcoal-600',
'hover:transform hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/10 hover:border-gray-400',
'dark-theme:hover:shadow-lg dark-theme:hover:shadow-black/30 dark-theme:hover:border-charcoal-700',
'focus:outline-none focus:ring-2 focus:ring-blue-500 dark-theme:focus:ring-blue-400'
'focus:outline-none focus:transform focus:-translate-y-0.5 focus:shadow-lg focus:shadow-black/10 dark-theme:focus:shadow-black/30'
Copy link
Contributor Author

@arjansingh arjansingh Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more in line with current design system.

],
// Div-specific styles
!interactive && [
Expand Down
15 changes: 8 additions & 7 deletions src/platform/assets/components/AssetFilterBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div :class="leftSideClasses" data-component-id="asset-filter-bar-left">
<MultiSelect
v-model="fileFormats"
label="File formats"
:label="$t('assetBrowser.fileFormats')"
:options="fileFormatOptions"
:class="selectClasses"
data-component-id="asset-filter-file-formats"
Expand All @@ -12,7 +12,7 @@

<MultiSelect
v-model="baseModels"
label="Base models"
:label="$t('assetBrowser.baseModels')"
:options="baseModelOptions"
:class="selectClasses"
data-component-id="asset-filter-base-models"
Expand All @@ -23,7 +23,7 @@
<div :class="rightSideClasses" data-component-id="asset-filter-bar-right">
<SingleSelect
v-model="sortBy"
label="Sort by"
:label="$t('assetBrowser.sortBy')"
:options="sortOptions"
:class="selectClasses"
data-component-id="asset-filter-sort"
Expand All @@ -43,6 +43,7 @@ import { ref } from 'vue'
import MultiSelect from '@/components/input/MultiSelect.vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
import type { SelectOption } from '@/components/input/types'
import { t } from '@/i18n'
import { cn } from '@/utils/tailwindUtil'

export interface FilterState {
Expand Down Expand Up @@ -74,10 +75,10 @@ const baseModelOptions = [
// TODO: Make sortOptions configurable via props
// Different asset types might need different sorting options
const sortOptions = [
{ name: 'A-Z', value: 'name-asc' },
{ name: 'Z-A', value: 'name-desc' },
{ name: 'Recent', value: 'recent' },
{ name: 'Popular', value: 'popular' }
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
{ name: t('assetBrowser.sortZA'), value: 'name-desc' },
{ name: t('assetBrowser.sortRecent'), value: 'recent' },
{ name: t('assetBrowser.sortPopular'), value: 'popular' }
]

const emit = defineEmits<{
Expand Down
47 changes: 38 additions & 9 deletions src/platform/assets/composables/useAssetBrowser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { computed, ref } from 'vue'

import { t } from '@/i18n'
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
import { d, t } from '@/i18n'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetFilenameSchema } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import {
getAssetBaseModel,
getAssetDescription
Expand Down Expand Up @@ -69,7 +70,7 @@ export function useAssetBrowser(assets: AssetItem[] = []) {

// Create display stats from API data
const stats = {
formattedDate: new Date(asset.created_at).toLocaleDateString(),
formattedDate: d(new Date(asset.created_at), { dateStyle: 'short' }),
downloadCount: undefined, // Not available in API
stars: undefined // Not available in API
}
Expand Down Expand Up @@ -162,12 +163,41 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
return filtered.map(transformAssetForDisplay)
})

// Actions
function selectAsset(asset: AssetDisplayItem): UUID {
/**
* Asset selection that fetches full details and executes callback with filename
* @param assetId - The asset ID to select and fetch details for
* @param onSelect - Optional callback to execute with the asset filename
*/
async function selectAssetWithCallback(
assetId: string,
onSelect?: (filename: string) => void
): Promise<void> {
if (import.meta.env.DEV) {
console.log('Asset selected:', asset.id, asset.name)
console.debug('Asset selected:', assetId)
}

if (!onSelect) {
return
}

try {
const detailAsset = await assetService.getAssetDetails(assetId)
const filename = detailAsset.user_metadata?.filename
const validatedFilename = assetFilenameSchema.safeParse(filename)
if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors,
'for asset:',
assetId
)
return
}

onSelect(validatedFilename.data)
} catch (error) {
console.error(`Failed to fetch asset details for ${assetId}:`, error)
}
return asset.id
}

return {
Expand All @@ -182,7 +212,6 @@ export function useAssetBrowser(assets: AssetItem[] = []) {
filteredAssets,

// Actions
selectAsset,
transformAssetForDisplay
selectAssetWithCallback
}
}
Loading