feat: add provider logo overlays to workflow template thumbnails (#8365)

## Summary

Add support for overlaying provider logos on workflow template
thumbnails at runtime.

## Changes

- **What**: 
  - Add `LogoInfo` interface and `logos` field to `TemplateInfo` type
  - Create `LogoOverlay.vue` component for rendering positioned logos
  - Fetch logo index from `templates/index_logo.json` in store
  - Add `getLogoUrl` helper to `useTemplateWorkflows` composable
  - Integrate `LogoOverlay` into `WorkflowTemplateSelectorDialog`

## Review Focus

- Logo positioning uses Tailwind classes (e.g. `absolute bottom-2
right-2`)
- Supports multiple logos per template with configurable size/opacity
- Gracefully handles missing logos (returns empty string, renders
nothing)
- Templates must explicitly declare logos - no magic inference from
models

## Dependencies

Requires separate PR in workflow_templates repo to:
1. Update `index.schema.json` with logos definition
2. Add `logos` field to templates in `index.json`

## Screenshots (if applicable)

<img width="869" height="719" alt="image"
src="https://github.com/user-attachments/assets/65ed1ee4-fbb4-42c9-95d4-7e37813b3655"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8365-feat-add-provider-logo-overlays-to-workflow-template-thumbnails-2f66d73d365081309236c6b991cb6f7b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Christian Byrne
2026-01-31 19:18:13 -08:00
committed by GitHub
parent 4f2872460c
commit 1bbbcfedf0
7 changed files with 349 additions and 7 deletions

View File

@@ -8,6 +8,8 @@ import type { NavGroupData, NavItemData } from '@/types/navTypes'
import { generateCategoryId, getCategoryIcon } from '@/utils/categoryUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { zLogoIndex } from '../schemas/templateSchema'
import type { LogoIndex } from '../schemas/templateSchema'
import type {
TemplateGroup,
TemplateInfo,
@@ -31,6 +33,7 @@ export const useWorkflowTemplatesStore = defineStore(
const customTemplates = shallowRef<{ [moduleName: string]: string[] }>({})
const coreTemplates = shallowRef<WorkflowTemplates[]>([])
const englishTemplates = shallowRef<WorkflowTemplates[]>([])
const logoIndex = shallowRef<LogoIndex>({})
const isLoaded = ref(false)
const knownTemplateNames = ref(new Set<string>())
@@ -475,15 +478,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 +504,36 @@ export const useWorkflowTemplatesStore = defineStore(
}
}
async function fetchLogoIndex(): Promise<LogoIndex> {
try {
const response = await api.fetchApi('/templates/index_logo.json')
const contentType = response.headers.get('content-type')
if (!contentType?.includes('application/json')) return {}
const data = await response.json()
const result = zLogoIndex.safeParse(data)
return result.success ? result.data : {}
} catch {
return {}
}
}
function getLogoUrl(provider: string): string {
const logoPath = logoIndex.value[provider]
if (!logoPath) return ''
// Validate path to prevent directory traversal and ensure safe file extensions
const safePathPattern = /^[a-zA-Z0-9_\-./]+\.(png|jpg|jpeg|svg|webp)$/i
if (
!safePathPattern.test(logoPath) ||
logoPath.includes('..') ||
logoPath.startsWith('/')
) {
return ''
}
return api.fileURL(`/templates/${logoPath}`)
}
function getEnglishMetadata(templateName: string): {
tags?: string[]
category?: string
@@ -534,7 +570,8 @@ export const useWorkflowTemplatesStore = defineStore(
loadWorkflowTemplates,
knownTemplateNames,
getTemplateByName,
getEnglishMetadata
getEnglishMetadata,
getLogoUrl
}
}
)

View File

@@ -0,0 +1,5 @@
import { z } from 'zod'
export const zLogoIndex = z.record(z.string(), z.string())
export type LogoIndex = z.infer<typeof zLogoIndex>

View File

@@ -1,3 +1,16 @@
export interface LogoInfo {
/** Provider name(s) matching index_logo.json. String for single, array for stacked logos. */
provider: string | string[]
/** Custom label text. If omitted, defaults to provider names joined with " & " */
label?: string
/** Gap between stacked logos in pixels. Negative for overlap effect. Default: -6 */
gap?: number
/** Tailwind positioning classes */
position?: string
/** Opacity 0-1, default 0.85 */
opacity?: number
}
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 {