diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
index acb39e2e8..81cff0e8c 100644
--- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
+++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue
@@ -251,6 +251,11 @@
"
/>
+
diff --git a/src/components/templates/thumbnails/LogoOverlay.test.ts b/src/components/templates/thumbnails/LogoOverlay.test.ts
new file mode 100644
index 000000000..d49f0c03c
--- /dev/null
+++ b/src/components/templates/thumbnails/LogoOverlay.test.ts
@@ -0,0 +1,95 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it } from 'vitest'
+
+import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
+import type { LogoInfo } from '@/platform/workflow/templates/types/template'
+
+describe('LogoOverlay', () => {
+ const mockGetLogoUrl = (provider: string) => `/logos/${provider}.png`
+
+ const mountOverlay = (logos: LogoInfo[], props = {}) => {
+ return mount(LogoOverlay, {
+ props: {
+ logos,
+ getLogoUrl: mockGetLogoUrl,
+ ...props
+ }
+ })
+ }
+
+ it('renders nothing when logos array is empty', () => {
+ const wrapper = mountOverlay([])
+ expect(wrapper.findAll('img')).toHaveLength(0)
+ })
+
+ it('renders a logo with correct src and alt', () => {
+ const wrapper = mountOverlay([{ provider: 'Google' }])
+ const img = wrapper.find('img')
+ expect(img.attributes('src')).toBe('/logos/Google.png')
+ expect(img.attributes('alt')).toBe('Google')
+ })
+
+ it('renders multiple logos', () => {
+ const wrapper = mountOverlay([
+ { provider: 'Google' },
+ { provider: 'OpenAI' },
+ { provider: 'Stability' }
+ ])
+ expect(wrapper.findAll('img')).toHaveLength(3)
+ })
+
+ it('applies default position when not specified', () => {
+ const wrapper = mountOverlay([{ provider: 'Google' }])
+ const container = wrapper.find('div')
+ expect(container.classes()).toContain('bottom-2')
+ expect(container.classes()).toContain('right-2')
+ })
+
+ it('applies custom position from logo config', () => {
+ const wrapper = mountOverlay([
+ { provider: 'Google', position: 'top-2 left-2' }
+ ])
+ const container = wrapper.find('div')
+ expect(container.classes()).toContain('top-2')
+ expect(container.classes()).toContain('left-2')
+ })
+
+ it('applies default medium size class', () => {
+ const wrapper = mountOverlay([{ provider: 'Google' }])
+ const img = wrapper.find('img')
+ expect(img.classes()).toContain('h-8')
+ expect(img.classes()).toContain('w-8')
+ })
+
+ it('applies small size class', () => {
+ const wrapper = mountOverlay([{ provider: 'Google', size: 'sm' }])
+ const img = wrapper.find('img')
+ expect(img.classes()).toContain('h-6')
+ expect(img.classes()).toContain('w-6')
+ })
+
+ it('applies large size class', () => {
+ const wrapper = mountOverlay([{ provider: 'Google', size: 'lg' }])
+ const img = wrapper.find('img')
+ expect(img.classes()).toContain('h-12')
+ expect(img.classes()).toContain('w-12')
+ })
+
+ it('applies default opacity', () => {
+ const wrapper = mountOverlay([{ provider: 'Google' }])
+ const container = wrapper.find('div')
+ expect(container.attributes('style')).toContain('opacity: 0.9')
+ })
+
+ it('applies custom opacity', () => {
+ const wrapper = mountOverlay([{ provider: 'Google', opacity: 0.5 }])
+ const container = wrapper.find('div')
+ expect(container.attributes('style')).toContain('opacity: 0.5')
+ })
+
+ it('images are not draggable', () => {
+ const wrapper = mountOverlay([{ provider: 'Google' }])
+ const img = wrapper.find('img')
+ expect(img.attributes('draggable')).toBe('false')
+ })
+})
diff --git a/src/components/templates/thumbnails/LogoOverlay.vue b/src/components/templates/thumbnails/LogoOverlay.vue
new file mode 100644
index 000000000..91568613a
--- /dev/null
+++ b/src/components/templates/thumbnails/LogoOverlay.vue
@@ -0,0 +1,39 @@
+
+
+
![]()
+
+
+
+
diff --git a/src/platform/workflow/templates/composables/useTemplateWorkflows.ts b/src/platform/workflow/templates/composables/useTemplateWorkflows.ts
index a5ee9c3b9..09c384961 100644
--- a/src/platform/workflow/templates/composables/useTemplateWorkflows.ts
+++ b/src/platform/workflow/templates/composables/useTemplateWorkflows.ts
@@ -158,7 +158,6 @@ export function useTemplateWorkflows() {
*/
const fetchTemplateJson = async (id: string, sourceModule: string) => {
if (sourceModule === 'default') {
- // Default templates provided by frontend are served on this separate endpoint
return fetch(api.fileURL(`/templates/${id}.json`)).then((r) => r.json())
} else {
return fetch(
@@ -167,6 +166,13 @@ export function useTemplateWorkflows() {
}
}
+ /**
+ * Gets logo URL for a provider name
+ */
+ const getLogoUrl = (provider: string): string => {
+ return workflowTemplatesStore.getLogoUrl(provider)
+ }
+
return {
// State
selectedTemplate,
@@ -183,6 +189,7 @@ export function useTemplateWorkflows() {
getTemplateThumbnailUrl,
getTemplateTitle,
getTemplateDescription,
- loadWorkflowTemplate
+ loadWorkflowTemplate,
+ getLogoUrl
}
}
diff --git a/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts b/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts
index f96872f79..0c0e087be 100644
--- a/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts
+++ b/src/platform/workflow/templates/repositories/workflowTemplatesStore.ts
@@ -9,6 +9,7 @@ import { generateCategoryId, getCategoryIcon } from '@/utils/categoryUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import type {
+ LogoIndex,
TemplateGroup,
TemplateInfo,
WorkflowTemplates
@@ -31,6 +32,7 @@ export const useWorkflowTemplatesStore = defineStore(
const customTemplates = shallowRef<{ [moduleName: string]: string[] }>({})
const coreTemplates = shallowRef([])
const englishTemplates = shallowRef([])
+ const logoIndex = shallowRef({})
const isLoaded = ref(false)
const knownTemplateNames = ref(new Set())
@@ -475,15 +477,18 @@ export const useWorkflowTemplatesStore = defineStore(
customTemplates.value = await api.getWorkflowTemplates()
const locale = i18n.global.locale.value
- const [coreResult, englishResult] = await Promise.all([
- api.getCoreWorkflowTemplates(locale),
- isCloud && locale !== 'en'
- ? api.getCoreWorkflowTemplates('en')
- : Promise.resolve([])
- ])
+ const [coreResult, englishResult, logoIndexResult] =
+ await Promise.all([
+ api.getCoreWorkflowTemplates(locale),
+ isCloud && locale !== 'en'
+ ? api.getCoreWorkflowTemplates('en')
+ : Promise.resolve([]),
+ fetchLogoIndex()
+ ])
coreTemplates.value = coreResult
englishTemplates.value = englishResult
+ logoIndex.value = logoIndexResult
const coreNames = coreTemplates.value.flatMap((category) =>
category.templates.map((template) => template.name)
@@ -498,6 +503,22 @@ export const useWorkflowTemplatesStore = defineStore(
}
}
+ async function fetchLogoIndex(): Promise {
+ try {
+ const response = await fetch(api.fileURL('/templates/index_logo.json'))
+ if (!response.ok) return {}
+ return await response.json()
+ } catch {
+ return {}
+ }
+ }
+
+ function getLogoUrl(provider: string): string {
+ const logoPath = logoIndex.value[provider]
+ if (!logoPath) return ''
+ return api.fileURL(`/templates/${logoPath}`)
+ }
+
function getEnglishMetadata(templateName: string): {
tags?: string[]
category?: string
@@ -534,7 +555,8 @@ export const useWorkflowTemplatesStore = defineStore(
loadWorkflowTemplates,
knownTemplateNames,
getTemplateByName,
- getEnglishMetadata
+ getEnglishMetadata,
+ getLogoUrl
}
}
)
diff --git a/src/platform/workflow/templates/types/template.ts b/src/platform/workflow/templates/types/template.ts
index 1a9d839f8..5f5f1cc7b 100644
--- a/src/platform/workflow/templates/types/template.ts
+++ b/src/platform/workflow/templates/types/template.ts
@@ -1,3 +1,16 @@
+export interface LogoInfo {
+ /** Provider name matching index_logo.json */
+ provider: string
+ /** Tailwind positioning classes */
+ position?: string
+ /** Size: 'sm' (24px), 'md' (32px), 'lg' (48px) */
+ size?: 'sm' | 'md' | 'lg'
+ /** Opacity 0-1, default 0.9 */
+ opacity?: number
+}
+
+export type LogoIndex = Record
+
export interface TemplateInfo {
name: string
/**
@@ -47,6 +60,10 @@ export interface TemplateInfo {
* If not specified, the template will be included on all distributions.
*/
includeOnDistributions?: TemplateIncludeOnDistributionEnum[]
+ /**
+ * Logo overlays to display on the template thumbnail.
+ */
+ logos?: LogoInfo[]
}
export enum TemplateIncludeOnDistributionEnum {