Skip to content

Commit 7498b79

Browse files
author
chennan
committed
show asset library suggestions in AssetGenModal
1 parent 0039ae7 commit 7498b79

10 files changed

Lines changed: 370 additions & 60 deletions

File tree

spx-gui/src/components/asset/gen/AssetGen.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import type { AssetData } from '@/apis/asset'
23
import type { AssetGenModel } from '@/models/spx/common/asset'
34
import { SpriteGen } from '@/models/spx/gen/sprite-gen'
45
import { BackdropGen } from '@/models/spx/gen/backdrop-gen'
@@ -8,11 +9,13 @@ import BackdropGenComp from './backdrop/BackdropGen.vue'
89
defineProps<{
910
gen: AssetGenModel
1011
descriptionPlaceholder?: string
12+
enableLibrarySearch?: boolean
1113
}>()
1214
1315
defineEmits<{
1416
collapse: []
1517
finished: []
18+
assetPicked: [AssetData]
1619
}>()
1720
</script>
1821

@@ -21,13 +24,17 @@ defineEmits<{
2124
v-if="gen instanceof SpriteGen"
2225
:gen="gen"
2326
:description-placeholder="descriptionPlaceholder"
27+
:enable-library-search="enableLibrarySearch"
2428
@collapse="$emit('collapse')"
2529
@finished="$emit('finished')"
30+
@asset-picked="$emit('assetPicked', $event)"
2631
/>
2732
<BackdropGenComp
2833
v-else-if="gen instanceof BackdropGen"
2934
:gen="gen"
3035
:description-placeholder="descriptionPlaceholder"
36+
:enable-library-search="enableLibrarySearch"
3137
@finished="$emit('finished')"
38+
@asset-picked="$emit('assetPicked', $event)"
3239
/>
3340
</template>

spx-gui/src/components/asset/gen/AssetGenModal.vue

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { computed, ref } from 'vue'
33
import { UIModal, UIModalClose, useConfirmDialog, type ModalTransformOrigin } from '@/components/ui'
44
import { useI18n } from '@/utils/i18n'
55
import { useMessageHandle } from '@/utils/exception'
6-
import { AssetType } from '@/apis/asset'
6+
import { AssetType, type AssetData } from '@/apis/asset'
77
import type { SpxProject } from '@/models/spx/project'
8-
import type { AssetGenModel, AssetModel } from '@/models/spx/common/asset'
8+
import { addAssetToProject, type AssetGenModel, type AssetModel } from '@/models/spx/common/asset'
99
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
1010
import { useAssetGen, addAssetGenResultToProject } from './useAssetGen'
1111
import AssetGenComp from './AssetGen.vue'
@@ -71,6 +71,22 @@ const handleGenFinished = useMessageHandle(
7171
}
7272
).fn
7373
74+
const handleAssetPicked = useMessageHandle(
75+
async (asset: AssetData) => {
76+
const added = await editorCtx.state.history.doAction(
77+
{
78+
name: { en: `Add ${entityMessage.value.en}`, zh: `添加${entityMessage.value.zh}` }
79+
},
80+
() => addAssetToProject(asset, props.project)
81+
)
82+
emit('resolved', added)
83+
},
84+
{
85+
en: 'Failed to add asset',
86+
zh: '素材添加失败'
87+
}
88+
)
89+
7490
const handleModalClose = useMessageHandle(
7591
async () => {
7692
const em = entityMessage.value
@@ -106,8 +122,10 @@ const handleModalClose = useMessageHandle(
106122
v-if="assetGen != null"
107123
class="asset-gen"
108124
:gen="assetGen"
125+
enable-library-search
109126
@collapse="handleGenCollapse"
110127
@finished="handleGenFinished"
128+
@asset-picked="handleAssetPicked.fn"
111129
/>
112130
</UIModal>
113131
</template>

spx-gui/src/components/asset/gen/backdrop/BackdropGen.vue

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
11
<script setup lang="ts">
22
import { computed, ref } from 'vue'
33
import { UIButton } from '@/components/ui'
4+
import { AssetType, type AssetData } from '@/apis/asset'
45
import type { Backdrop } from '@/models/spx/backdrop'
56
import type { BackdropGen } from '@/models/spx/gen/backdrop-gen'
67
import { capture, useMessageHandle } from '@/utils/exception'
8+
import { useI18n } from '@/utils/i18n'
79
import { humanizeTimeLeft } from '../common/time-left'
810
import BackdropSettingInput from './BackdropSettingsInput.vue'
911
import LayoutWithPreview from '../common/LayoutWithPreview.vue'
1012
import ImagePreview from '../common/ImagePreview.vue'
1113
import ImageSelector from '../common/ImageSelector.vue'
14+
import AssetSuggestions from '../common/AssetSuggestions.vue'
15+
import { useAssetSuggestions, buildGenSettingsKeyword } from '../common/useAssetSuggestions'
1216
import BackdropImageItem from './BackdropImageItem.vue'
17+
import BackdropItem from '@/components/asset/library/BackdropItem.vue'
1318
14-
const props = defineProps<{
15-
gen: BackdropGen
16-
descriptionPlaceholder?: string
17-
}>()
19+
const props = withDefaults(
20+
defineProps<{
21+
gen: BackdropGen
22+
descriptionPlaceholder?: string
23+
enableLibrarySearch?: boolean
24+
}>(),
25+
{
26+
descriptionPlaceholder: undefined,
27+
enableLibrarySearch: false
28+
}
29+
)
1830
1931
const emit = defineEmits<{
2032
finished: [Backdrop]
33+
assetPicked: [AssetData]
2134
}>()
2235
2336
const canSubmit = computed(() => props.gen.image != null)
37+
const i18n = useI18n()
2438
const handleSubmit = useMessageHandle(
2539
async () => {
2640
const backdrop = await props.gen.finish()
@@ -43,6 +57,20 @@ function handleImageSelect(index: number) {
4357
props.gen.setImageIndex(index)
4458
hasPreview.value = true
4559
}
60+
61+
const isLibrarySearchEnabled = computed(
62+
() => props.enableLibrarySearch && props.gen.imagesGenState.status === 'initial'
63+
)
64+
65+
const {
66+
suggestions,
67+
selected: selectedAsset,
68+
toggle: toggleSelectedAsset
69+
} = useAssetSuggestions(
70+
AssetType.Backdrop,
71+
() => buildGenSettingsKeyword(i18n, props.gen.settings),
72+
() => isLibrarySearchEnabled.value
73+
)
4674
</script>
4775

4876
<template>
@@ -58,6 +86,25 @@ function handleImageSelect(index: number) {
5886
:disabled="handleSubmit.isLoading.value"
5987
:description-placeholder="descriptionPlaceholder"
6088
/>
89+
<AssetSuggestions
90+
v-if="isLibrarySearchEnabled"
91+
:type="AssetType.Backdrop"
92+
:suggestions="suggestions"
93+
:selected="selectedAsset"
94+
@toggle="toggleSelectedAsset"
95+
>
96+
<template #item="{ asset, selected, onClick }">
97+
<BackdropItem :asset="asset" :selected="selected" @click="onClick" />
98+
</template>
99+
<template #tip>
100+
{{
101+
$t({
102+
en: `There are related backdrops in the library. You can choose the one you like or continue generating.`,
103+
zh: `素材库中已有相关的背景,可以选择你喜欢的背景直接使用,或者继续生成。`
104+
})
105+
}}
106+
</template>
107+
</AssetSuggestions>
61108
<ImageSelector
62109
:state="gen.imagesGenState"
63110
:selected="gen.imageIndex"
@@ -92,6 +139,19 @@ function handleImageSelect(index: number) {
92139
</LayoutWithPreview>
93140
<footer class="footer">
94141
<UIButton
142+
v-if="selectedAsset != null"
143+
v-radar="{
144+
name: 'Use',
145+
desc: 'Click to use the selected library asset'
146+
}"
147+
color="primary"
148+
size="large"
149+
@click="emit('assetPicked', selectedAsset!)"
150+
>
151+
{{ $t({ en: 'Use', zh: '采用' }) }}
152+
</UIButton>
153+
<UIButton
154+
v-else
95155
v-radar="{
96156
name: 'Use',
97157
desc: 'Finish and use the generated backdrop in the project'
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup lang="ts">
2+
import { AssetType, type AssetData } from '@/apis/asset'
3+
4+
const props = defineProps<{
5+
type: AssetType
6+
suggestions: AssetData[]
7+
selected: AssetData | null
8+
}>()
9+
10+
const emit = defineEmits<{
11+
toggle: [AssetData]
12+
}>()
13+
14+
function isSelected(asset: AssetData) {
15+
return props.selected?.id === asset.id
16+
}
17+
</script>
18+
19+
<template>
20+
<div class="asset-suggestions">
21+
<template v-if="suggestions.length > 0">
22+
<ul class="list">
23+
<template v-for="asset in suggestions" :key="asset.id">
24+
<slot name="item" :asset="asset" :selected="isSelected(asset)" :on-click="() => emit('toggle', asset)"></slot>
25+
</template>
26+
</ul>
27+
<p class="tip">
28+
<slot name="tip"></slot>
29+
</p>
30+
</template>
31+
</div>
32+
</template>
33+
34+
<style lang="scss" scoped>
35+
.asset-suggestions {
36+
height: 170px;
37+
display: flex;
38+
flex-direction: column;
39+
align-items: center;
40+
gap: 12px;
41+
}
42+
43+
.list {
44+
display: flex;
45+
align-items: center;
46+
gap: 8px;
47+
flex-wrap: nowrap;
48+
justify-content: center;
49+
list-style: none;
50+
padding: 0;
51+
margin: 0;
52+
}
53+
54+
.tip {
55+
text-align: center;
56+
font-size: 12px;
57+
color: var(--ui-color-hint-2);
58+
}
59+
</style>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ref, shallowRef, watch, type Ref, type ShallowRef } from 'vue'
2+
import { debounce } from 'lodash'
3+
import type { I18n } from '@/utils/i18n'
4+
import { listAsset, Visibility, type AssetData, type AssetType } from '@/apis/asset'
5+
import { ArtStyle, Perspective, ownerAll } from '@/apis/common'
6+
import type { BackdropSettings, SpriteSettings } from '@/apis/aigc'
7+
import { artStyleOptions, perspectiveOptions, spriteCategoryOptions, backdropCategoryOptions } from './param-settings/data'
8+
9+
export function useAssetSuggestions(
10+
type: AssetType,
11+
keyword: () => string,
12+
enabled: () => boolean
13+
): {
14+
suggestions: ShallowRef<AssetData[]>
15+
isLoading: Ref<boolean>
16+
selected: ShallowRef<AssetData | null>
17+
toggle: (asset: AssetData) => void
18+
} {
19+
const suggestions = shallowRef<AssetData[]>([])
20+
const isLoading = ref(false)
21+
const selected = shallowRef<AssetData | null>(null)
22+
23+
const doSearch = debounce(async (keyword: string) => {
24+
isLoading.value = true
25+
try {
26+
const result = await listAsset({
27+
keyword,
28+
type,
29+
pageSize: 4,
30+
pageIndex: 1,
31+
visibility: Visibility.Public,
32+
owner: ownerAll
33+
})
34+
suggestions.value = result.data
35+
} catch {
36+
suggestions.value = []
37+
} finally {
38+
isLoading.value = false
39+
}
40+
}, 500)
41+
42+
watch(
43+
[keyword, enabled],
44+
([kw, isEnabled]) => {
45+
if (!isEnabled || kw.trim() === '') {
46+
doSearch.cancel()
47+
suggestions.value = []
48+
selected.value = null
49+
isLoading.value = false
50+
return
51+
}
52+
doSearch(kw.trim())
53+
},
54+
{ immediate: true }
55+
)
56+
57+
function toggle(asset: AssetData) {
58+
selected.value = selected.value?.id === asset.id ? null : asset
59+
}
60+
61+
return { suggestions, isLoading, selected, toggle }
62+
}
63+
64+
/**
65+
* Build a search keyword from gen settings.
66+
* Combines description with non-unspecified category/artStyle/perspective labels.
67+
*/
68+
// TODO: refine the keyword building logic.
69+
export function buildGenSettingsKeyword(
70+
i18n: I18n,
71+
settings: SpriteSettings | BackdropSettings
72+
) {
73+
const desc = settings.description.trim()
74+
if (desc === '') return ''
75+
76+
const parts: string[] = [desc]
77+
78+
const categoryOption =
79+
spriteCategoryOptions.find((o) => o.value === settings.category) ??
80+
backdropCategoryOptions.find((o) => o.value === settings.category)
81+
if (categoryOption != null) {
82+
parts.push(i18n.t(categoryOption.label))
83+
}
84+
if (settings.artStyle != null && settings.artStyle !== ArtStyle.Unspecified) {
85+
const option = artStyleOptions.find((o) => o.value === settings.artStyle)
86+
if (option != null) parts.push(i18n.t(option.label))
87+
}
88+
if (settings.perspective != null && settings.perspective !== Perspective.Unspecified) {
89+
const option = perspectiveOptions.find((o) => o.value === settings.perspective)
90+
if (option != null) parts.push(i18n.t(option.label))
91+
}
92+
return parts.join(' ')
93+
}

spx-gui/src/components/asset/gen/sprite/SpriteGen.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed } from 'vue'
33
import { computedShallowReactive, useComputedDisposable } from '@/utils/utils'
44
import { useI18n } from '@/utils/i18n'
55
import { useNetwork } from '@/utils/network'
6+
import type { AssetData } from '@/apis/asset'
67
import type { SpriteGen } from '@/models/spx/gen/sprite-gen'
78
import type { Sprite } from '@/models/spx/sprite'
89
import { getSignedInUsername } from '@/stores/user'
@@ -16,11 +17,13 @@ import SpriteGenPhaseContent from './SpriteGenPhaseContent.vue'
1617
const props = defineProps<{
1718
gen: SpriteGen
1819
descriptionPlaceholder?: string
20+
enableLibrarySearch?: boolean
1921
}>()
2022
2123
const emit = defineEmits<{
2224
collapse: []
2325
finished: [Sprite]
26+
assetPicked: [AssetData]
2427
}>()
2528
2629
const i18n = useI18n()
@@ -51,7 +54,13 @@ provideLocalEditorCtx(editorCtxInGen)
5154
@collapse="emit('collapse')"
5255
@finished="emit('finished', $event)"
5356
/>
54-
<SpriteGenPhaseSettings v-else :gen="gen" :description-placeholder="descriptionPlaceholder" />
57+
<SpriteGenPhaseSettings
58+
v-else
59+
:gen="gen"
60+
:description-placeholder="descriptionPlaceholder"
61+
:enable-library-search="enableLibrarySearch"
62+
@asset-picked="emit('assetPicked', $event)"
63+
/>
5564
</template>
5665

5766
<style lang="scss" scoped></style>

0 commit comments

Comments
 (0)