From b2b1b4dfbbb05f3e91be5d62240d62e6dfd936bc Mon Sep 17 00:00:00 2001 From: Maanil Verma Date: Sat, 18 Jul 2026 18:13:03 +0530 Subject: [PATCH] feat(onboarding): backfill Getting Started cards from fallback templates A curated id missing from the backend data was dropped silently, shrinking the grid below the four skeletons it renders. - Fill the shortfall, in order, from FALLBACK_TEMPLATE_IDS - Resolve tutorial thumbnails against a loaded template so a missing one no longer renders an empty src --- .../firstRunTour/GettingStartedScreen.test.ts | 47 ++++++++++++++++++- .../firstRunTour/GettingStartedScreen.vue | 34 ++++++++++---- .../extensions/firstRunTour/tutorialCards.ts | 8 ++++ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/renderer/extensions/firstRunTour/GettingStartedScreen.test.ts b/src/renderer/extensions/firstRunTour/GettingStartedScreen.test.ts index b86e4e1e20..fb26fe243b 100644 --- a/src/renderer/extensions/firstRunTour/GettingStartedScreen.test.ts +++ b/src/renderer/extensions/firstRunTour/GettingStartedScreen.test.ts @@ -65,7 +65,7 @@ vi.mock('./useFirstRunTourController', () => ({ })) import GettingStartedScreen from './GettingStartedScreen.vue' -import { tutorialCards } from './tutorialCards' +import { FALLBACK_TEMPLATE_IDS, tutorialCards } from './tutorialCards' import { useOnboardingEntryStore } from '@/platform/workflow/persistence/onboardingEntryStore' import enMessages from '@/locales/en/main.json' @@ -106,6 +106,51 @@ describe('GettingStartedScreen', () => { } }) + it('backfills only the shortfall when curated templates are missing', () => { + mocks.templatesByName.delete('video_ltx2_3_i2v') + FALLBACK_TEMPLATE_IDS.forEach((id) => + mocks.templatesByName.set(id, makeTemplate(id, id)) + ) + renderScreen() + + expect(screen.getAllByTestId(/^getting-started-card-/)).toHaveLength( + CARD_IDS.length + ) + expect( + screen.getByTestId(`getting-started-card-${FALLBACK_TEMPLATE_IDS[0]}`) + ).toBeTruthy() + expect( + screen.queryByTestId(`getting-started-card-${FALLBACK_TEMPLATE_IDS[1]}`) + ).toBeNull() + }) + + it('leaves the grid untouched when every curated template resolves', () => { + FALLBACK_TEMPLATE_IDS.forEach((id) => + mocks.templatesByName.set(id, makeTemplate(id, id)) + ) + renderScreen() + + for (const id of CARD_IDS) { + expect(screen.getByTestId(`getting-started-card-${id}`)).toBeTruthy() + } + expect( + screen.queryByTestId(`getting-started-card-${FALLBACK_TEMPLATE_IDS[0]}`) + ).toBeNull() + }) + + it('keeps tutorial thumbnails resolvable when their template is missing', async () => { + mocks.templatesByName.delete('video_ltx2_3_i2v') + renderScreen() + mocks.getTemplateThumbnailUrl.mockClear() + await userEvent.click(screen.getByRole('tab', { name: /tutorials/i })) + + // Every tutorial resolves a thumbnail from some loaded template, so none + // renders an empty src. + expect(mocks.getTemplateThumbnailUrl).toHaveBeenCalledTimes( + tutorialCards.length + ) + }) + it('loads the templates store when opened unloaded so cards can resolve', () => { mocks.templatesLoaded = false renderScreen() diff --git a/src/renderer/extensions/firstRunTour/GettingStartedScreen.vue b/src/renderer/extensions/firstRunTour/GettingStartedScreen.vue index 3bb42e496b..318308cc9d 100644 --- a/src/renderer/extensions/firstRunTour/GettingStartedScreen.vue +++ b/src/renderer/extensions/firstRunTour/GettingStartedScreen.vue @@ -80,9 +80,11 @@ class="grid grid-cols-2 gap-5 sm:grid-cols-3 lg:grid-cols-4" > ('screenRef') -const cards = computed(() => - CURATED_TEMPLATE_IDS.map((id) => templatesStore.getTemplateByName(id)).filter( - (template) => template !== undefined +function resolveTemplates(ids: readonly string[]) { + return ids + .map((id) => templatesStore.getTemplateByName(id)) + .filter((template) => template !== undefined) +} + +/** Tops the grid back up to its skeleton count when curated data is incomplete. */ +const cards = computed(() => { + const curated = resolveTemplates(CURATED_TEMPLATE_IDS) + const chosen = new Set(curated.map((template) => template.name)) + const backfill = resolveTemplates(FALLBACK_TEMPLATE_IDS).filter( + (template) => !chosen.has(template.name) ) -) + return [...curated, ...backfill].slice(0, CURATED_TEMPLATE_IDS.length) +}) // Cards and per-card loads no-op until the templates store is loaded; nothing // else loads it on this path, so load it (and focus the takeover) on open. @@ -177,8 +190,13 @@ watch( { immediate: true } ) -function tutorialThumbnail(id: TutorialCard['thumbnailTemplate']) { - const template = templatesStore.getTemplateByName(id) +function tutorialThumbnail( + id: TutorialCard['thumbnailTemplate'], + index: number +) { + const template = + templatesStore.getTemplateByName(id) ?? + cards.value[index % cards.value.length] return template ? getTemplateThumbnailUrl(template, 'default') : '' } diff --git a/src/renderer/extensions/firstRunTour/tutorialCards.ts b/src/renderer/extensions/firstRunTour/tutorialCards.ts index 49f363f847..81d133656e 100644 --- a/src/renderer/extensions/firstRunTour/tutorialCards.ts +++ b/src/renderer/extensions/firstRunTour/tutorialCards.ts @@ -11,6 +11,14 @@ export const CURATED_TEMPLATE_IDS = [ 'video_wan2_2_14B_i2v' ] as const satisfies readonly CuratedTemplateId[] +/** Fills the grid, in order, when a curated template is missing from the data. */ +export const FALLBACK_TEMPLATE_IDS = [ + 'gsc_advanced_3_2', + 'image_qwen_image_edit_2509', + 'templates-qwen_multiangle.app', + 'gsc_advanced_3_1' +] as const + type LoadedTemplateId = (typeof CURATED_TEMPLATE_IDS)[number] /**