Compare commits

...

4 Commits

Author SHA1 Message Date
Michael B
1610bcaacb refactor: extract ShowcaseCard component from models showcase
Move per-card markup out of ModelsShowcaseSection into a dedicated
ShowcaseCard component (mirroring GalleryCard). Switch the card to a
uniform h-80 height, animate the badge color on hover, and drop the
now-conflicting aspect-ratio classes that caused cards to overflow
their grid columns.
2026-05-22 13:51:14 -04:00
Michael B
5b6508e103 feat: add featured AI models showcase to models page 2026-05-21 17:21:06 -04:00
Michael B
563e759041 feat: add creations and product showcase sections to models page 2026-05-21 15:17:51 -04:00
Michael B
984b6914d3 feat: add models page hero section
Adds ModelsHeroSection component, models list pages (en and zh-CN),
and accompanying i18n translations.
2026-05-21 14:49:20 -04:00
7 changed files with 511 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { Locale } from '../../i18n/translations'
import type { GalleryItem } from '../gallery/GallerySection.vue'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
import GalleryCard from '../gallery/GalleryCard.vue'
import GalleryDetailModal from '../gallery/GalleryDetailModal.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const modelName = 'Grok'
const ctaHref = 'https://comfy.org/workflows/model/grok'
const items: GalleryItem[] = [
{
video: 'https://media.comfy.org/videos/compressed_512/eye.webm',
title: 'Until Our Eye Interlink harajuku',
userAlias: 'ShaneF Motion Design',
teamAlias: 'ThinkDiffusion',
tool: 'Grok Imagine',
href: 'https://www.thinkdiffusion.com/studio#success-stories-anta'
},
{
image: 'https://media.comfy.org/website/gallery/gallery.webp',
title: 'Amber Astronaut',
userAlias: 'Yogo',
teamAlias: '',
tool: 'Grok Imagine',
href: 'https://de.linkedin.com/in/milan-kastenmueller-18778a174'
},
{
video: 'https://media.comfy.org/videos/compressed_512/cigarette.webm',
title: 'Autopoiesis',
userAlias: 'Yogo',
teamAlias: 'Visual Frisson',
tool: 'Grok Imagine',
href: 'https://www.instagram.com/visualfrisson/?hl=en'
},
{
video: 'https://media.comfy.org/videos/compressed_512/kyrie.webm',
title: 'Origins',
userAlias: 'ShaneF Motion Design',
teamAlias: 'ThinkDiffusion',
tool: 'Grok Imagine',
href: 'https://vimeo.com/1021360563'
},
{
image: 'https://media.comfy.org/website/gallery/desert.webp',
title: 'Desert Landing',
userAlias: 'Yogo',
teamAlias: '',
tool: 'Grok Imagine',
href: 'https://de.linkedin.com/in/milan-kastenmueller-18778a174'
}
]
const modalOpen = ref(false)
const modalIndex = ref(0)
function openDetail(index: number) {
modalIndex.value = index
modalOpen.value = true
}
const title = t('models.list.creations.title', locale).replace(
'{name}',
modelName
)
const ctaLabel = t('models.list.creations.cta', locale)
</script>
<template>
<section
data-testid="model-creations"
class="flex flex-col items-center px-4 pt-12 pb-20 lg:px-20"
>
<h2
class="text-primary-comfy-canvas max-w-4xl text-center text-3xl font-light tracking-tight lg:text-5xl"
>
{{ title }}
</h2>
<BrandButton
:href="ctaHref"
variant="solid"
size="lg"
class="mt-16 px-8 py-4 uppercase"
>
{{ ctaLabel }}
</BrandButton>
<div class="mt-20 hidden w-full flex-col gap-2 lg:flex">
<div class="grid grid-cols-2 gap-2">
<GalleryCard
v-for="(item, i) in items.slice(0, 2)"
:key="i"
:item
:locale
@click="openDetail(i)"
/>
</div>
<div v-if="items.length > 2" class="grid grid-cols-3 gap-2">
<GalleryCard
v-for="(item, i) in items.slice(2, 5)"
:key="i + 2"
:item
:locale
@click="openDetail(i + 2)"
/>
</div>
</div>
<div
class="rounded-5xl bg-transparency-white-t4 mt-12 flex w-full flex-col gap-6 p-2 lg:hidden"
>
<GalleryCard
v-for="(item, i) in items"
:key="i"
:item
:locale
mobile
@click="openDetail(i)"
/>
</div>
<GalleryDetailModal
v-if="modalOpen"
:items
:initial-index="modalIndex"
:locale
@close="modalOpen = false"
/>
</section>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
const {
locale = 'en',
modelName,
ctaHref,
mediaSrc,
mediaAlt = ''
} = defineProps<{
locale?: Locale
modelName: string
ctaHref: string
mediaSrc: string
mediaAlt?: string
}>()
</script>
<template>
<section class="flex flex-col items-center px-6 pt-36 pb-16 text-center">
<h1
class="text-primary-comfy-canvas max-w-4xl text-4xl font-light tracking-tight lg:text-6xl"
>
{{ modelName }} in <span class="text-primary-comfy-yellow">ComfyUI</span>
</h1>
<p
class="text-primary-comfy-canvas mt-6 max-w-2xl text-sm text-pretty lg:text-base"
>
{{ t('hero.subtitle', locale) }}
</p>
<BrandButton
:href="ctaHref"
variant="solid"
size="lg"
class="mt-10 px-8 py-4 uppercase"
>
{{ t('models.list.heroCta', locale).replace('{name}', modelName) }}
</BrandButton>
<div class="mt-16 w-full max-w-5xl">
<img
:src="mediaSrc"
:alt="mediaAlt"
class="rounded-4.5xl size-full object-cover"
/>
</div>
</section>
</template>

View File

@@ -0,0 +1,114 @@
<script setup lang="ts">
import type { Locale, TranslationKey } from '../../i18n/translations'
import { externalLinks, getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
import ShowcaseCard from './ShowcaseCard.vue'
export type ModelCard = {
titleKey: TranslationKey
slug: string
imageSrc: string
badgeIcon?: string
badgeText?: string
layoutClass: string
objectPosition?: string
}
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
const modelCards: ModelCard[] = [
{
titleKey: 'models.showcase.card.grokImagine',
slug: 'grok-image',
imageSrc: 'https://media.comfy.org/website/cloud/ai-models/grok-video.webm',
badgeIcon: '/icons/ai-models/grok.svg',
layoutClass: 'lg:col-span-6'
},
{
titleKey: 'models.showcase.card.nanoBananaPro',
slug: 'nano-banana',
imageSrc:
'https://media.comfy.org/website/cloud/ai-models/nano-banana-pro.webp',
badgeIcon: '/icons/ai-models/gemini.svg',
layoutClass: 'lg:col-span-6',
objectPosition: 'center 20%'
},
{
titleKey: 'models.showcase.card.ltx23',
slug: 'ltxv-api',
imageSrc: 'https://media.comfy.org/website/gallery/desert.webp',
badgeText: 'ltx',
layoutClass: 'lg:col-span-4'
},
{
titleKey: 'models.showcase.card.qwenAdvancedEdit',
slug: 'qwen-image-fp8-e4m3fn',
imageSrc:
'https://media.comfy.org/website/cloud/ai-models/qwen-image-edit.webp',
badgeIcon: '/icons/ai-models/qwen.svg',
layoutClass: 'lg:col-span-4'
},
{
titleKey: 'models.showcase.card.wan22TextToVideo',
slug: 'wan-api',
imageSrc: 'https://media.comfy.org/website/cloud/ai-models/wan-22.webm',
badgeIcon: '/icons/ai-models/wan.svg',
layoutClass: 'lg:col-span-4'
}
]
function modelHref(slug: string): string {
return `${routes.models}/${slug}`
}
</script>
<template>
<section class="px-4 py-24 lg:px-20 lg:py-40">
<div class="mx-auto flex w-full max-w-7xl flex-col items-center">
<p
class="text-primary-comfy-yellow text-center text-sm font-bold tracking-widest uppercase"
>
{{ t('models.showcase.label', locale) }}
</p>
<h2
class="text-primary-comfy-canvas text-3.5xl/tight mt-8 max-w-4xl text-center font-light whitespace-pre-line lg:text-5xl"
>
{{ t('models.showcase.heading', locale) }}
</h2>
<p
class="text-primary-comfy-canvas mt-8 max-w-xl text-center text-sm font-light lg:text-base/snug"
>
{{ t('models.showcase.subtitle', locale) }}
</p>
<div class="mt-24 w-full">
<div class="bg-transparency-white-t4 rounded-4xl p-2 lg:p-1.5">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-12">
<ShowcaseCard
v-for="card in modelCards"
:key="card.titleKey"
:card="card"
:href="modelHref(card.slug)"
:locale="locale"
:class="card.layoutClass"
/>
</div>
</div>
</div>
<BrandButton
:href="externalLinks.workflows"
variant="outline"
class="mt-8 w-full max-w-md text-center lg:w-auto"
>
{{ t('models.showcase.cta', locale) }}
</BrandButton>
</div>
</section>
</template>

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import type { Locale } from '../../i18n/translations'
import type { ModelCard } from './ModelsShowcaseSection.vue'
import { t } from '../../i18n/translations'
const {
card,
href,
locale = 'en'
} = defineProps<{
card: ModelCard
href: string
locale?: Locale
}>()
const badgeBase =
'bg-white/20 text-white backdrop-blur-sm transition-colors duration-300 ease-in-out group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink'
</script>
<template>
<a
:href="href"
target="_blank"
rel="noopener noreferrer"
class="group relative h-80 cursor-pointer overflow-hidden rounded-4xl bg-black/40"
>
<video
v-if="card.imageSrc.endsWith('.webm')"
:src="card.imageSrc"
:aria-label="t(card.titleKey, locale)"
:style="
card.objectPosition
? { objectPosition: card.objectPosition }
: undefined
"
class="size-full object-cover transition-transform duration-600 ease-in-out group-hover:scale-105"
autoplay
loop
muted
playsinline
/>
<img
v-else
:src="card.imageSrc"
:alt="t(card.titleKey, locale)"
:style="
card.objectPosition
? { objectPosition: card.objectPosition }
: undefined
"
class="size-full object-cover transition-transform duration-600 ease-in-out group-hover:scale-105"
loading="lazy"
decoding="async"
/>
<div
class="absolute inset-0 bg-linear-to-t from-black/50 via-black/5 to-black/35"
/>
<div
:class="
cn(
'absolute top-5 right-5 flex h-12 min-w-12 items-center justify-center px-3 lg:top-6 lg:right-6',
badgeBase,
'rounded-2xl'
)
"
>
<span
v-if="card.badgeIcon"
class="inline-block size-6 bg-current"
:style="{
maskImage: `url(${card.badgeIcon})`,
maskSize: 'contain',
maskRepeat: 'no-repeat',
maskPosition: 'center'
}"
/>
<span
v-else-if="card.badgeText"
class="text-xs font-bold tracking-wider lowercase"
>
{{ card.badgeText }}
</span>
</div>
<p
class="text-primary-warm-white absolute inset-x-6 bottom-6 text-2xl/tight font-light whitespace-pre-line drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)] lg:top-6 lg:right-auto lg:bottom-auto lg:text-3xl"
>
{{ t(card.titleKey, locale) }}
</p>
</a>
</template>

View File

@@ -4194,6 +4194,76 @@ const translations = {
'zh-CN': '支持的模型'
},
// Models list page (/models)
'models.list.label': { en: 'MODELS', 'zh-CN': '模型' },
'models.list.heroCta': {
en: 'Try {name} Now',
'zh-CN': '立即试用 {name}'
},
'models.list.creations.title': {
en: '{name} Image and Video Creations',
'zh-CN': '{name} 图像与视频创作'
},
'models.list.creations.cta': {
en: 'Explore Workflows',
'zh-CN': '探索工作流'
},
'models.list.heroTitle.before': {
en: 'Run the worlds leading AI models in',
'zh-CN': '在以下平台运行世界领先的 AI 模型'
},
'models.list.heroSubtitle': {
en: 'From open-source diffusion checkpoints to partner APIs — every major model, with community workflow templates ready to run.',
'zh-CN':
'从开源扩散模型到合作伙伴 API涵盖每一个主流模型并附带可直接运行的社区工作流模板。'
},
'models.list.card.workflows': {
en: '{count} workflows',
'zh-CN': '{count} 个工作流'
},
'models.list.contact.label': {
en: 'COMFY HUB',
'zh-CN': 'COMFY HUB'
},
'models.showcase.label': { en: 'AI MODELS', 'zh-CN': 'AI 模型' },
'models.showcase.heading': {
en: 'Run the worlds\nleading AI models',
'zh-CN': '运行全球领先的\nAI 模型'
},
'models.showcase.subtitle': {
en: 'New models are added as they launch.',
'zh-CN': '新模型发布后会第一时间上线。'
},
'models.showcase.cta': {
en: 'EXPLORE WORKFLOWS',
'zh-CN': '探索工作流'
},
'models.showcase.card.grokImagine': {
en: 'Grok Imagine',
'zh-CN': 'Grok Imagine'
},
'models.showcase.card.nanoBananaPro': {
en: 'Nano Banana Pro',
'zh-CN': 'Nano Banana Pro'
},
'models.showcase.card.ltx23': {
en: 'LTX 2.3',
'zh-CN': 'LTX 2.3'
},
'models.showcase.card.qwenAdvancedEdit': {
en: 'Advanced image\nediting with Qwen',
'zh-CN': '使用 Qwen 进行\n高级图像编辑'
},
'models.showcase.card.wan22TextToVideo': {
en: 'Wan 2.2\ntext to video',
'zh-CN': 'Wan 2.2\n文字转视频'
},
'models.list.contact.heading': {
en: 'Pick a model and explore what the community has built. <a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">Browse Comfy Hub</a> for the newest workflows.',
'zh-CN':
'选择一个模型,浏览社区的创作成果。<a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">访问 Comfy Hub</a> 查看最新工作流。'
},
// Payment status pages
'payment.success.label': {
en: 'PAYMENT',

View File

@@ -0,0 +1,22 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import ModelsHeroSection from '../components/models/ModelsHeroSection.vue'
import ModelCreationsSection from '../components/models/ModelCreationsSection.vue'
import ModelsShowcaseSection from '../components/models/ModelsShowcaseSection.vue'
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
---
<BaseLayout
title="Models — Comfy"
description="Run the world's leading AI models in ComfyUI. Browse every supported model with community workflow templates ready to run."
>
<ModelsHeroSection
modelName="Grok Imagine"
ctaHref="/p/supported-models/grok-image"
mediaSrc="https://media.comfy.org/website/gallery/gallery.webp"
mediaAlt="Grok Imagine output created with ComfyUI"
/>
<ModelCreationsSection client:load />
<ModelsShowcaseSection />
<ProductShowcaseSection client:load />
</BaseLayout>

View File

@@ -0,0 +1,23 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import ModelsHeroSection from '../../components/models/ModelsHeroSection.vue'
import ModelCreationsSection from '../../components/models/ModelCreationsSection.vue'
import ModelsShowcaseSection from '../../components/models/ModelsShowcaseSection.vue'
import ProductShowcaseSection from '../../components/home/ProductShowcaseSection.vue'
---
<BaseLayout
title="模型 — Comfy"
description="在 ComfyUI 中运行世界领先的 AI 模型。浏览所有支持的模型及社区工作流模板。"
>
<ModelsHeroSection
locale="zh-CN"
modelName="Grok Imagine"
ctaHref="/zh-CN/p/supported-models/grok-image"
mediaSrc="https://media.comfy.org/website/gallery/gallery.webp"
mediaAlt="Grok Imagine output created with ComfyUI"
/>
<ModelCreationsSection client:load locale="zh-CN" />
<ModelsShowcaseSection locale="zh-CN" />
<ProductShowcaseSection client:load locale="zh-CN" />
</BaseLayout>