improvement(integrations, models): ui/ux (#4105)

* improvement(integrations, models): ui/ux

* fix(models, integrations): dedup ChevronArrow/provider colors, fix UTC date rendering

- Extract PROVIDER_COLORS and getProviderColor to model-colors.ts to eliminate
  identical definitions in model-comparison-charts and model-timeline-chart
- Remove duplicate private ChevronArrow from integration-card; import the
  exported one from model-primitives instead
- Add timeZone: 'UTC' to formatShortDate so ISO date-only strings (parsed as
  UTC midnight) render the correct calendar day in all timezones

* refactor(models): rename model-colors.ts to consts.ts

* improvement(models): derive provider colors/resellers from definitions, reorient FAQs to agent builder

Dynamic data:
- Add `color` and `isReseller` fields to ProviderDefinition interface
- Move brand colors for all 10 providers into their definitions
- Mark 6 reseller providers (Azure, Bedrock, Vertex, OpenRouter, Fireworks)
- consts.ts now derives color map from MODEL_CATALOG_PROVIDERS
- model-comparison-charts derives RESELLER_PROVIDERS from catalog
- Fix deepseek name: Deepseek → DeepSeek; remove now-redundant
  PROVIDER_NAME_OVERRIDES and getProviderDisplayName from utils
- Add color/isReseller fields to CatalogProvider; clean up duplicate
  providerDisplayName in searchText array

FAQs:
- Replace all 4 main-page FAQs with 5 agent-builder-oriented ones
  covering model selection, context windows, pricing, tool use, and
  how to use models in a Sim agent workflow
- buildProviderFaqs: add conditional tool use FAQ per provider
- buildModelFaqs: add bestFor FAQ (conditional on field presence);
  improve context window answer to explain agent implications;
  tighten capabilities answer wording

* chore(models): remove model-colors.ts (superseded by consts.ts)

* update footer

---------

Co-authored-by: waleed <walif6@gmail.com>
This commit is contained in:
Emir Karabeg
2026-04-10 20:46:44 -07:00
committed by GitHub
co-authored by waleed
parent bad78ccb59
commit c1d788ce94
45 changed files with 2396 additions and 1564 deletions
+1 -1
View File
@@ -161,7 +161,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
<h3 className='font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em]'>
{p.title}
</h3>
<p className='line-clamp-2 text-[#F6F6F0]/50 text-sm leading-[150%]'>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{p.description}
</p>
</div>
+3 -3
View File
@@ -110,7 +110,7 @@ export default async function BlogIndex({
<h1 className='text-balance font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'>
Latest from Sim
</h1>
<p className='max-w-[360px] font-[430] font-season text-[#F6F6F0]/50 text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
<p className='max-w-[540px] font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
Announcements, insights, and guides for building AI agent workflows.
</p>
</div>
@@ -152,7 +152,7 @@ export default async function BlogIndex({
<h3 className='font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em]'>
{p.title}
</h3>
<p className='line-clamp-2 text-[#F6F6F0]/50 text-sm leading-[150%]'>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{p.description}
</p>
</div>
@@ -191,7 +191,7 @@ export default async function BlogIndex({
<h3 className='font-[430] font-season text-base text-white leading-tight tracking-[-0.01em] lg:text-lg'>
{p.title}
</h3>
<p className='line-clamp-2 text-[#F6F6F0]/40 text-sm leading-[150%]'>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{p.description}
</p>
</div>
@@ -63,10 +63,8 @@ const INTEGRATION_LINKS: FooterItem[] = [
{ label: 'Linear', href: 'https://docs.sim.ai/tools/linear', external: true },
{ label: 'Airtable', href: 'https://docs.sim.ai/tools/airtable', external: true },
{ label: 'Firecrawl', href: 'https://docs.sim.ai/tools/firecrawl', external: true },
{ label: 'Pinecone', href: 'https://docs.sim.ai/tools/pinecone', external: true },
{ label: 'Discord', href: 'https://docs.sim.ai/tools/discord', external: true },
{ label: 'Microsoft Teams', href: 'https://docs.sim.ai/tools/microsoft_teams', external: true },
{ label: 'Outlook', href: 'https://docs.sim.ai/tools/outlook', external: true },
{ label: 'Telegram', href: 'https://docs.sim.ai/tools/telegram', external: true },
]
@@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronDown } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
@@ -15,46 +16,67 @@ interface LandingFAQProps {
export function LandingFAQ({ faqs }: LandingFAQProps) {
const [openIndex, setOpenIndex] = useState<number | null>(0)
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
return (
<div className='divide-y divide-[var(--landing-border)]'>
<div>
{faqs.map(({ question, answer }, index) => {
const isOpen = openIndex === index
const isHovered = hoveredIndex === index
const showDivider = index > 0 && hoveredIndex !== index && hoveredIndex !== index - 1
return (
<div key={question}>
<div
className={cn(
'h-px w-full bg-[var(--landing-bg-elevated)]',
index === 0 || !showDivider ? 'invisible' : 'visible'
)}
/>
<button
type='button'
onClick={() => setOpenIndex(isOpen ? null : index)}
className='flex w-full items-start justify-between gap-4 py-5 text-left'
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
className='-mx-6 flex w-[calc(100%+3rem)] items-center justify-between gap-4 px-6 py-4 text-left transition-colors hover:bg-[var(--landing-bg-elevated)]'
aria-expanded={isOpen}
>
<span
className={cn(
'font-[500] text-[15px] leading-snug transition-colors',
'text-[15px] leading-snug tracking-[-0.02em] transition-colors',
isOpen
? 'text-[var(--landing-text)]'
: 'text-[var(--landing-text-muted)] hover:text-[var(--landing-text)]'
: 'text-[var(--landing-text-body)] hover:text-[var(--landing-text)]'
)}
>
{question}
</span>
<ChevronDown
className={cn(
'mt-0.5 h-4 w-4 shrink-0 text-[#555] transition-transform duration-200',
'h-3 w-3 shrink-0 text-[var(--landing-text-subtle)] transition-transform duration-200',
isOpen ? 'rotate-180' : 'rotate-0'
)}
aria-hidden='true'
/>
</button>
{isOpen && (
<div className='pb-5'>
<p className='text-[14px] text-[var(--landing-text-muted)] leading-[1.75]'>
{answer}
</p>
</div>
)}
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
className='overflow-hidden'
>
<div className='pt-2 pb-4'>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[1.75]'>
{answer}
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)
})}
@@ -0,0 +1,149 @@
import type { ComponentType, SVGProps } from 'react'
import Link from 'next/link'
import {
AgentIcon,
ApiIcon,
McpIcon,
PackageSearchIcon,
TableIcon,
WorkflowIcon,
} from '@/components/icons'
interface ProductLink {
label: string
description: string
href: string
external?: boolean
icon: ComponentType<SVGProps<SVGSVGElement>>
}
interface SidebarLink {
label: string
href: string
external?: boolean
}
const PLATFORM: ProductLink[] = [
{
label: 'Workflows',
description: 'Visual AI automation builder',
href: 'https://docs.sim.ai/getting-started',
external: true,
icon: WorkflowIcon,
},
{
label: 'Agent',
description: 'Build autonomous AI agents',
href: 'https://docs.sim.ai/blocks/agent',
external: true,
icon: AgentIcon,
},
{
label: 'MCP',
description: 'Connect external tools',
href: 'https://docs.sim.ai/mcp',
external: true,
icon: McpIcon,
},
{
label: 'Knowledge Base',
description: 'Retrieval-augmented context',
href: 'https://docs.sim.ai/knowledgebase',
external: true,
icon: PackageSearchIcon,
},
{
label: 'Tables',
description: 'Structured data storage',
href: 'https://docs.sim.ai/tables',
external: true,
icon: TableIcon,
},
{
label: 'API',
description: 'Deploy workflows as endpoints',
href: 'https://docs.sim.ai/api-reference/getting-started',
external: true,
icon: ApiIcon,
},
]
const EXPLORE: SidebarLink[] = [
{ label: 'Models', href: '/models' },
{ label: 'Integrations', href: '/integrations' },
{ label: 'Changelog', href: '/changelog' },
{ label: 'Self-hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
]
function DropdownLink({ link }: { link: ProductLink }) {
const Icon = link.icon
const Tag = link.external ? 'a' : Link
const props = link.external
? { href: link.href, target: '_blank' as const, rel: 'noopener noreferrer' }
: { href: link.href }
return (
<Tag
{...props}
className='group/item flex items-start gap-2.5 rounded-[5px] px-2.5 py-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<Icon className='mt-0.5 h-[15px] w-[15px] shrink-0 text-[var(--landing-text-icon)]' />
<div className='flex flex-col'>
<span className='font-[430] font-season text-[13px] text-white leading-tight'>
{link.label}
</span>
<span className='font-season text-[12px] text-[var(--landing-text-subtle)] leading-[150%]'>
{link.description}
</span>
</div>
</Tag>
)
}
export function ProductDropdown() {
return (
<div className='flex w-[560px] rounded-[5px] border border-[var(--landing-bg-elevated)] bg-[var(--landing-bg)] shadow-overlay'>
<div className='flex-1 p-2'>
<div className='mb-1 px-2.5 pt-1'>
<span className='font-[430] font-season text-[11px] text-[var(--landing-text-subtle)] uppercase tracking-[0.08em]'>
Platform
</span>
<div className='mt-1.5 h-px bg-[var(--landing-bg-elevated)]' />
</div>
<div className='grid grid-cols-2'>
{PLATFORM.map((link) => (
<DropdownLink key={link.label} link={link} />
))}
</div>
</div>
<div className='w-px self-stretch bg-[var(--landing-bg-elevated)]' />
<div className='w-[160px] p-2'>
<div className='mb-1 px-2.5 pt-1'>
<span className='font-[430] font-season text-[11px] text-[var(--landing-text-subtle)] uppercase tracking-[0.08em]'>
Explore
</span>
<div className='mt-1.5 h-px bg-[var(--landing-bg-elevated)]' />
</div>
{EXPLORE.map((link) => {
const Tag = link.external ? 'a' : Link
const props = link.external
? { href: link.href, target: '_blank' as const, rel: 'noopener noreferrer' }
: { href: link.href }
return (
<Tag
key={link.label}
{...props}
className='block rounded-[5px] px-2.5 py-1.5 font-[430] font-season text-[13px] text-white transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
{link.label}
</Tag>
)
})}
</div>
</div>
)
}
@@ -2,13 +2,15 @@
import { useRouter } from 'next/navigation'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { cn } from '@/lib/core/utils/cn'
interface TemplateCardButtonProps {
prompt: string
className?: string
children: React.ReactNode
}
export function TemplateCardButton({ prompt, children }: TemplateCardButtonProps) {
export function TemplateCardButton({ prompt, className, children }: TemplateCardButtonProps) {
const router = useRouter()
function handleClick() {
@@ -17,11 +19,7 @@ export function TemplateCardButton({ prompt, children }: TemplateCardButtonProps
}
return (
<button
type='button'
onClick={handleClick}
className='group flex w-full flex-col items-start rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5 text-left transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
>
<button type='button' onClick={handleClick} className={cn('w-full text-left', className)}>
{children}
</button>
)
@@ -283,7 +283,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
}
return (
<>
<section className='bg-[var(--landing-bg)]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
@@ -301,440 +301,434 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
{/* Breadcrumb */}
<nav
aria-label='Breadcrumb'
className='mb-10 flex items-center gap-2 text-[#555] text-[13px]'
>
<Link href='/' className='transition-colors hover:text-[var(--landing-text-muted)]'>
Home
</Link>
<span aria-hidden='true'>/</span>
{/* Hero */}
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<div className='mb-6'>
<Link
href='/integrations'
className='transition-colors hover:text-[var(--landing-text-muted)]'
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
Integrations
</Link>
<span aria-hidden='true'>/</span>
<span className='text-[var(--landing-text-muted)]'>{name}</span>
</nav>
{/* Hero */}
<section aria-labelledby='integration-heading' className='mb-16'>
<div className='mb-6 flex items-center gap-5'>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-16 w-16 rounded-xl'
iconClassName='h-8 w-8'
fallbackClassName='text-[26px]'
<svg
className='h-3 w-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
aria-hidden='true'
/>
<div>
<p className='mb-0.5 text-[#555] text-[12px]'>Integration</p>
<h1
id='integration-heading'
className='font-[500] text-[36px] text-[var(--landing-text)] leading-tight sm:text-[44px]'
>
{name}
</h1>
</div>
</div>
<p className='mb-8 max-w-[700px] text-[17px] text-[var(--landing-text-muted)] leading-[1.7]'>
{description}
</p>
{/* CTAs */}
<div className='flex flex-wrap gap-2'>
<a
href='https://sim.ai'
className='inline-flex h-[32px] items-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Start building free
</a>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex h-[32px] items-center gap-1.5 rounded-[5px] border border-[var(--landing-border-strong)] px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
View docs
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
<line
x1='1'
y1='5'
x2='10'
y2='5'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
strokeWidth='1.33'
strokeLinecap='square'
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
/>
<path
d='M6.5 2L3.5 5L6.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
/>
</svg>
Back to Integrations
</Link>
</div>
{/* Hero content */}
<div className='mb-6 flex items-center gap-5'>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-12 w-12 rounded-[5px]'
iconClassName='h-6 w-6'
fallbackClassName='text-[20px]'
aria-hidden='true'
/>
<div>
<h1
id='integration-heading'
className='text-[28px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] lg:text-[44px]'
>
{name}
</h1>
</div>
</div>
<p className='mb-8 max-w-[700px] text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em]'>
{description}
</p>
{/* CTAs */}
<div className='flex flex-wrap gap-2'>
<Link
href='/signup'
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Start building free
</Link>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='group/link inline-flex h-[32px] items-center gap-1.5 rounded-[5px] border border-[var(--landing-border-strong)] px-2.5 font-season text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
View docs
<svg
aria-hidden='true'
className='-rotate-45 h-3 w-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
>
<line
x1='0'
y1='5'
x2='9'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
/>
<path
d='M3.5 2L6.5 5L3.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='transition-transform duration-200 ease-out group-hover/link:translate-x-[30%]'
/>
</svg>
</a>
</div>
</div>
{/* Full-width divider */}
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
{/* Border-railed content */}
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
{/* Overview */}
{longDescription && (
<>
<section aria-labelledby='overview-heading' className='px-6 py-10'>
<h2
id='overview-heading'
className='mb-4 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
Overview
</h2>
<p className='text-[15px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
{longDescription}
</p>
</section>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
{/* How to automate */}
<section aria-labelledby='how-it-works-heading' className='px-6 py-10'>
<h2
id='how-it-works-heading'
className='mb-6 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
How to automate {name} with Sim
</h2>
<ol className='space-y-4' aria-label='Steps to set up automation'>
{[
{
step: '01',
title: 'Create a free account',
body: 'Sign up at sim.ai in seconds. No credit card required. Your workspace is ready immediately.',
},
{
step: '02',
title: `Add a ${name} block`,
body:
authType === 'oauth'
? `Open a workflow, drag a ${name} block onto the canvas, and connect your account with one-click OAuth.`
: authType === 'api-key'
? `Open a workflow, drag a ${name} block onto the canvas, and paste in your ${name} API key.`
: `Open a workflow, drag a ${name} block onto the canvas, and authenticate your account.`,
},
{
step: '03',
title: 'Configure, connect, and run',
body: `Pick the tool you need, wire in an AI agent for reasoning or data transformation, and run. Your ${name} automation is live.`,
},
].map(({ step, title, body }) => (
<li key={step} className='flex gap-4'>
<span
className='mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-[var(--landing-border-strong)] font-martian-mono text-[11px] text-[var(--landing-text-subtle)]'
aria-hidden='true'
>
{step}
</span>
<div>
<h3 className='mb-1 text-[15px] text-white tracking-[-0.02em]'>{title}</h3>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
{body}
</p>
</div>
</li>
))}
</ol>
</section>
{/* Two-column layout */}
<div className='grid grid-cols-1 gap-16 lg:grid-cols-[1fr_300px]'>
{/* Main column */}
<div className='min-w-0 space-y-16'>
{/* Overview */}
{longDescription && (
<section aria-labelledby='overview-heading'>
<h2
id='overview-heading'
className='mb-4 font-[500] text-[20px] text-[var(--landing-text)]'
>
Overview
</h2>
<p className='text-[15px] text-[var(--landing-text-muted)] leading-[1.8]'>
{longDescription}
</p>
</section>
)}
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{/* How to automate — targets "how to connect X" queries */}
<section aria-labelledby='how-it-works-heading'>
<h2
id='how-it-works-heading'
className='mb-6 font-[500] text-[20px] text-[var(--landing-text)]'
>
How to automate {name} with Sim
</h2>
<ol className='space-y-4' aria-label='Steps to set up automation'>
{[
{
step: '01',
title: 'Create a free account',
body: 'Sign up at sim.ai in seconds. No credit card required. Your workspace is ready immediately.',
},
{
step: '02',
title: `Add a ${name} block`,
body:
authType === 'oauth'
? `Open a workflow, drag a ${name} block onto the canvas, and connect your account with one-click OAuth.`
: authType === 'api-key'
? `Open a workflow, drag a ${name} block onto the canvas, and paste in your ${name} API key.`
: `Open a workflow, drag a ${name} block onto the canvas, and authenticate your account.`,
},
{
step: '03',
title: 'Configure, connect, and run',
body: `Pick the tool you need, wire in an AI agent for reasoning or data transformation, and run. Your ${name} automation is live.`,
},
].map(({ step, title, body }) => (
<li key={step} className='flex gap-4'>
<span
className='mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-[var(--landing-border-strong)] font-[500] text-[#555] text-[11px]'
aria-hidden='true'
>
{step}
</span>
<div>
<h3 className='mb-1 font-[500] text-[15px] text-[var(--landing-text)]'>
{title}
</h3>
<p className='text-[14px] text-[var(--landing-text-muted)] leading-relaxed'>
{body}
</p>
</div>
</li>
))}
</ol>
</section>
{/* Triggers */}
{triggers.length > 0 && (
<section aria-labelledby='triggers-heading'>
{/* Triggers — rows */}
{triggers.length > 0 && (
<section aria-labelledby='triggers-heading'>
<div className='px-6 pt-10 pb-4'>
<div className='mb-2 flex items-center gap-2.5'>
<span className='relative flex h-2 w-2' aria-hidden='true'>
<span className='absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75' />
<span className='relative inline-flex h-2 w-2 rounded-full bg-emerald-500' />
</span>
<h2
id='triggers-heading'
className='mb-2 font-[500] text-[20px] text-[var(--landing-text)]'
className='text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
Real-time triggers
</h2>
<p className='mb-4 text-[14px] text-[var(--landing-text-muted)] leading-relaxed'>
Connect a {name} webhook to Sim and your workflow fires the instant an event
happens — no polling, no delay. Sim receives the full event payload and makes
every field available as a variable inside your workflow.
</p>
{/* Event cards */}
<ul
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
aria-label={`${name} trigger events`}
>
{triggers.map((trigger) => (
<li
key={trigger.id}
className='rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4'
>
<div className='mb-2 flex items-center gap-2'>
<span className='inline-flex items-center gap-1 rounded-[4px] bg-[var(--landing-bg-elevated)] px-1.5 py-0.5 font-[500] text-[11px] text-[var(--landing-text)]'>
<svg
aria-hidden='true'
className='h-2.5 w-2.5'
fill='none'
stroke='currentColor'
strokeWidth={2.5}
viewBox='0 0 24 24'
>
<polygon points='13 2 3 14 12 14 11 22 21 10 12 10 13 2' />
</svg>
Event
</span>
</div>
<p className='font-[500] text-[13px] text-[var(--landing-text)]'>
{trigger.name}
</div>
<p className='text-[14px] text-[var(--landing-text-body)] leading-[150%] tracking-[0.02em]'>
Connect a {name} webhook to Sim and your workflow fires the instant an event happens
— no polling, no delay.
</p>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{triggers.map((trigger) => (
<div key={trigger.id}>
<div className='flex items-start gap-4 px-6 py-4'>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<p className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
{trigger.name}
</p>
{trigger.description && (
<p className='text-[12px] text-[var(--landing-text-muted)] leading-[150%]'>
{trigger.description}
</p>
{trigger.description && (
<p className='mt-1 text-[12px] text-[var(--landing-text-muted)] leading-relaxed'>
{trigger.description}
</p>
)}
</div>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</div>
))}
</section>
)}
{/* Workflow templates — horizontal cards */}
{matchingTemplates.length > 0 && (
<section aria-labelledby='templates-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='templates-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
Workflow templates
</h2>
<p className='text-[14px] text-[var(--landing-text-body)] tracking-[0.02em]'>
Ready-to-use workflows featuring {name}. Click any to build it instantly.
</p>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{(() => {
const isOdd = matchingTemplates.length % 2 === 1
const pairedTemplates = isOdd ? matchingTemplates.slice(0, -1) : matchingTemplates
const lastTemplate = isOdd ? matchingTemplates[matchingTemplates.length - 1] : null
const resolveTypes = (template: (typeof matchingTemplates)[number]) => [
integration.type,
...template.integrationBlockTypes.filter((bt) => bt !== integration.type),
]
const renderIcons = (allTypes: string[]) =>
allTypes.map((bt, idx) => {
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: byType.get(`${bt}_v3`)
? `${bt}_v3`
: bt
const int = byType.get(resolvedBt)
const ToolIcon = blockTypeToIconMap[resolvedBt]
return (
<span key={bt} className='inline-flex items-center gap-1.5'>
{idx > 0 && (
<span className='text-[#555] text-[11px]' aria-hidden='true'>
→
</span>
)}
</li>
))}
</ul>
</section>
)}
{/* Workflow templates */}
{matchingTemplates.length > 0 && (
<section aria-labelledby='templates-heading'>
<h2
id='templates-heading'
className='mb-2 font-[500] text-[20px] text-[var(--landing-text)]'
>
Workflow templates
</h2>
<p className='mb-6 text-[14px] text-[var(--landing-text-muted)]'>
Ready-to-use workflows featuring {name}. Click any to build it instantly.
</p>
<ul
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
aria-label='Workflow templates'
>
{matchingTemplates.map((template) => {
const allTypes = [
integration.type,
...template.integrationBlockTypes.filter((bt) => bt !== integration.type),
]
<IntegrationIcon
bgColor={int?.bgColor ?? '#333'}
name={int?.name ?? bt}
Icon={ToolIcon}
as='span'
className='h-6 w-6 rounded-[4px]'
iconClassName='h-3.5 w-3.5'
fallbackClassName='text-[10px]'
aria-hidden='true'
/>
</span>
)
})
return (
<>
{/* Paired rows of 2 */}
{Array.from({ length: Math.ceil(pairedTemplates.length / 2) }, (_, rowIdx) => {
const row = pairedTemplates.slice(rowIdx * 2, rowIdx * 2 + 2)
return (
<li key={template.title}>
<TemplateCardButton prompt={template.prompt}>
{/* Integration pills row */}
<div className='mb-3 flex flex-wrap items-center gap-1.5 text-[12px]'>
{allTypes.map((bt, idx) => {
// Templates may use unversioned keys (e.g. "notion") while the
// icon map has versioned keys ("notion_v2") — fall back to _v2.
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: bt
const int = byType.get(resolvedBt)
const intName = int?.name ?? bt
return (
<span key={bt} className='inline-flex items-center gap-1.5'>
{idx > 0 && (
<span className='text-[#555]' aria-hidden='true'>
→
</span>
)}
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[var(--landing-bg-elevated)] px-1.5 py-0.5 font-[500] text-[var(--landing-text)]'>
<IntegrationIcon
bgColor={int?.bgColor ?? '#6B7280'}
name={intName}
Icon={blockTypeToIconMap[resolvedBt]}
as='span'
className='h-3.5 w-3.5 rounded-[2px]'
iconClassName='h-2.5 w-2.5'
aria-hidden='true'
/>
{intName}
</span>
</span>
)
})}
</div>
<p className='mb-1 font-[500] text-[14px] text-[var(--landing-text)]'>
{template.title}
</p>
<p className='mt-3 text-[#555] text-[13px] transition-colors group-hover:text-[var(--landing-text-muted)]'>
Try this workflow →
</p>
</TemplateCardButton>
</li>
<div key={rowIdx}>
<nav
aria-label={`Template row ${rowIdx + 1}`}
className='flex flex-col sm:flex-row'
>
{row.map((template) => (
<TemplateCardButton
key={template.title}
prompt={template.prompt}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<div className='flex items-center gap-1.5'>
{renderIcons(resolveTypes(template))}
</div>
<div className='flex flex-col gap-2'>
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
{template.title}
</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{template.prompt}
</p>
</div>
</TemplateCardButton>
))}
</nav>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</div>
)
})}
</ul>
</section>
)}
{/* Tools */}
{operations.length > 0 && (
<section aria-labelledby='tools-heading'>
<h2
id='tools-heading'
className='mb-2 font-[500] text-[20px] text-[var(--landing-text)]'
>
Supported tools
</h2>
<p className='mb-6 text-[14px] text-[var(--landing-text-muted)]'>
{operations.length} {name} tool{operations.length === 1 ? '' : 's'} available in
Sim
</p>
<ul
className='grid grid-cols-1 gap-2 sm:grid-cols-2'
aria-label={`${name} supported tools`}
>
{operations.map((op) => (
<li
key={op.name}
className='rounded-[6px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-3.5 py-3'
>
<p className='font-[500] text-[13px] text-[var(--landing-text)]'>{op.name}</p>
{op.description && (
<p className='mt-0.5 text-[#555] text-[12px] leading-relaxed'>
{op.description}
</p>
)}
</li>
))}
</ul>
</section>
)}
{/* FAQ */}
<section aria-labelledby='faq-heading'>
<h2
id='faq-heading'
className='mb-8 font-[500] text-[20px] text-[var(--landing-text)]'
>
Frequently asked questions
</h2>
<IntegrationFAQ faqs={faqs} />
</section>
</div>
{/* Sidebar */}
<aside className='space-y-5' aria-label='Integration details'>
{/* Quick details */}
<div className='rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5'>
<h3 className='mb-4 font-[500] text-[14px] text-[var(--landing-text)]'>Details</h3>
<dl className='space-y-3 text-[13px]'>
{operations.length > 0 && (
<div>
<dt className='text-[#555]'>Tools</dt>
<dd className='text-[var(--landing-text)]'>{operations.length} supported</dd>
</div>
)}
{triggers.length > 0 && (
<div>
<dt className='text-[#555]'>Triggers</dt>
<dd className='text-[var(--landing-text)]'>{triggers.length} available</dd>
</div>
)}
<div>
<dt className='text-[#555]'>Auth</dt>
<dd className='text-[var(--landing-text)]'>
{authType === 'oauth'
? 'One-click OAuth'
: authType === 'api-key'
? 'API key'
: 'None required'}
</dd>
</div>
<div>
<dt className='text-[#555]'>Pricing</dt>
<dd className='text-[var(--landing-text)]'>Free to start</dd>
</div>
</dl>
<div className='mt-5 flex flex-col gap-2'>
<a
href='https://sim.ai'
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] font-[430] font-season text-[13px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Get started free
</a>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='flex h-[32px] w-full items-center justify-center gap-1.5 rounded-[5px] border border-[var(--landing-border-strong)] font-[430] font-season text-[13px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
View docs
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
</div>
</div>
{/* Related integrations — internal linking for SEO */}
<div className='rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5'>
{relatedIntegrations.length > 0 && (
<>
<h3 className='mb-4 font-[500] text-[14px] text-[var(--landing-text)]'>
Related integrations
</h3>
<ul className='space-y-2'>
{relatedIntegrations.map((rel) => (
<li key={rel.slug}>
<Link
href={`/integrations/${rel.slug}`}
className='flex items-center gap-2.5 rounded-[6px] p-1.5 text-[13px] text-[var(--landing-text-muted)] transition-colors hover:bg-[var(--landing-bg-elevated)] hover:text-[var(--landing-text)]'
>
<IntegrationIcon
bgColor={rel.bgColor}
name={rel.name}
Icon={blockTypeToIconMap[rel.type]}
as='span'
className='h-6 w-6 rounded-[4px]'
iconClassName='h-3.5 w-3.5'
fallbackClassName='text-[10px]'
aria-hidden='true'
/>
{rel.name}
</Link>
</li>
))}
</ul>
{/* Last template as a full-width row when odd */}
{lastTemplate && (
<>
<TemplateCardButton
prompt={lastTemplate.prompt}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<div className='flex items-center gap-1.5'>
{renderIcons(resolveTypes(lastTemplate))}
</div>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
{lastTemplate.title}
</h3>
<p className='line-clamp-1 text-[12px] text-[var(--landing-text-muted)] leading-[150%]'>
{lastTemplate.prompt}
</p>
</div>
</TemplateCardButton>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
</>
)}
<Link
href='/integrations'
className={`block text-[#555] text-[12px] transition-colors hover:text-[var(--landing-text-muted)]${relatedIntegrations.length > 0 ? ' mt-4' : ''}`}
)
})()}
</section>
)}
{/* Supported tools — rows */}
{operations.length > 0 && (
<section aria-labelledby='tools-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='tools-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
All integrations →
</Link>
Supported tools
</h2>
<p className='text-[14px] text-[var(--landing-text-body)] tracking-[0.02em]'>
{operations.length} {name} tool{operations.length === 1 ? '' : 's'} available in Sim
</p>
</div>
</aside>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{operations.map((op) => (
<div key={op.name}>
<div className='flex items-start gap-4 px-6 py-4'>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<p className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
{op.name}
</p>
{op.description && (
<p className='text-[12px] text-[var(--landing-text-muted)] leading-[150%]'>
{op.description}
</p>
)}
</div>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</div>
))}
</section>
)}
{/* FAQ — full width */}
<section aria-labelledby='faq-heading' className='px-6 py-10'>
<h2
id='faq-heading'
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em]'
>
Frequently asked questions
</h2>
<IntegrationFAQ faqs={faqs} />
</section>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{/* Related integrations — horizontal cards with vertical dividers (blog featured pattern) */}
{relatedIntegrations.length > 0 && (
<>
<nav aria-label='Related integrations' className='flex flex-col sm:flex-row'>
{relatedIntegrations.slice(0, 4).map((rel) => (
<Link
key={rel.slug}
href={`/integrations/${rel.slug}`}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<IntegrationIcon
bgColor={rel.bgColor}
name={rel.name}
Icon={blockTypeToIconMap[rel.type]}
as='span'
className='h-10 w-10 rounded-[5px]'
aria-hidden='true'
/>
<div className='flex flex-col gap-2'>
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>
{rel.name}
</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{rel.description}
</p>
</div>
</Link>
))}
</nav>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
{/* Bottom CTA */}
<section
aria-labelledby='cta-heading'
className='mt-20 rounded-xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-8 text-center sm:p-12'
>
{/* Logo pair: Sim × Integration */}
<section aria-labelledby='cta-heading' className='px-6 py-16 text-center'>
<div className='mx-auto mb-6 flex items-center justify-center gap-3'>
<Image
src='/brandbook/logo/small.png'
@@ -776,22 +770,25 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
</div>
<h2
id='cta-heading'
className='mb-3 font-[500] text-[28px] text-[var(--landing-text)] sm:text-[34px]'
className='mb-3 text-[28px] text-white leading-[100%] tracking-[-0.02em] sm:text-[34px]'
>
Start automating {name} today
</h2>
<p className='mx-auto mb-8 max-w-[480px] text-[16px] text-[var(--landing-text-muted)] leading-relaxed'>
<p className='mx-auto mb-8 max-w-[480px] text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em]'>
Build your first AI workflow with {name} in minutes. Connect to every tool your team
uses. Free to start — no credit card required.
</p>
<a
href='https://sim.ai'
className='inline-flex h-[32px] items-center rounded-[5px] border border-[var(--white)] bg-[var(--white)] px-2.5 font-[430] font-season text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
<Link
href='/signup'
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Build for free →
</a>
Build for free
</Link>
</section>
</div>
</>
{/* Closing full-width divider */}
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
</section>
)
}
@@ -1,7 +1,7 @@
import type { ComponentType, SVGProps } from 'react'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import type { Integration } from '@/app/(landing)/integrations/data/types'
import { ChevronArrow } from '@/app/(landing)/models/components/model-primitives'
import { IntegrationIcon } from './integration-icon'
interface IntegrationCardProps {
@@ -9,49 +9,76 @@ interface IntegrationCardProps {
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
}
/**
* Featured integration card — matches blog featured post pattern.
* Used in flex rows separated by border-l dividers.
*/
export function IntegrationCard({ integration, IconComponent }: IntegrationCardProps) {
const { slug, name, description, bgColor, operationCount, triggerCount } = integration
const { slug, name, description, bgColor } = integration
return (
<Link
href={`/integrations/${slug}`}
className='group flex flex-col rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
aria-label={`${name} integration`}
className='group/link flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='mb-3 h-10 w-10 rounded-lg'
className='h-10 w-10 rounded-[5px]'
aria-hidden='true'
/>
{/* Name */}
<h3 className='mb-1 font-[500] text-[14px] text-[var(--landing-text)] leading-snug'>
{name}
</h3>
{/* Description — clamped to 2 lines */}
<p className='mb-3 line-clamp-2 flex-1 text-[12px] text-[var(--landing-text-muted)] leading-relaxed'>
{description}
</p>
{/* Footer row */}
<div className='flex flex-wrap items-center gap-1.5'>
{operationCount > 0 && (
<Badge className='border-0 bg-[#333] text-[11px] text-[var(--landing-text-muted)]'>
{operationCount} {operationCount === 1 ? 'tool' : 'tools'}
</Badge>
)}
{triggerCount > 0 && (
<Badge className='border-0 bg-[#333] text-[11px] text-[var(--landing-text-muted)]'>
{triggerCount} {triggerCount === 1 ? 'trigger' : 'triggers'}
</Badge>
)}
<span className='ml-auto text-[#555] text-[12px] transition-colors group-hover:text-[var(--landing-text-muted)]'>
Learn more →
</span>
<div className='flex flex-col gap-2'>
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{name}</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{description}
</p>
</div>
</Link>
)
}
interface IntegrationRowProps {
integration: Integration
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
}
/**
* Integration list row — matches blog remaining post pattern.
* Each row followed by an h-px divider.
*/
export function IntegrationRow({ integration, IconComponent }: IntegrationRowProps) {
const { slug, name, description, bgColor } = integration
return (
<>
<Link
href={`/integrations/${slug}`}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
aria-label={`${name} integration`}
>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-8 w-8 shrink-0 rounded-[5px]'
iconClassName='h-4 w-4'
fallbackClassName='text-[13px]'
aria-hidden='true'
/>
{/* Name + description */}
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>{name}</h3>
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
{description}
</p>
</div>
{/* Animated arrow */}
<ChevronArrow />
</Link>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)
}
@@ -4,12 +4,11 @@ import { useMemo, useState } from 'react'
import { Input } from '@/components/emcn'
import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mapping'
import type { Integration } from '@/app/(landing)/integrations/data/types'
import { IntegrationCard } from './integration-card'
import { IntegrationRow } from './integration-card'
const CATEGORY_LABELS: Record<string, string> = {
ai: 'AI',
analytics: 'Analytics',
automation: 'Automation',
communication: 'Communication',
crm: 'CRM',
'customer-support': 'Customer Support',
@@ -21,12 +20,10 @@ const CATEGORY_LABELS: Record<string, string> = {
email: 'Email',
'file-storage': 'File Storage',
hr: 'HR',
media: 'Media',
productivity: 'Productivity',
'sales-intelligence': 'Sales Intelligence',
sales: 'Sales',
search: 'Search',
security: 'Security',
social: 'Social',
other: 'Other',
} as const
@@ -41,8 +38,10 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
const availableCategories = useMemo(() => {
const counts = new Map<string, number>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
if (i.integrationTypes) {
for (const t of i.integrationTypes) {
counts.set(t, (counts.get(t) || 0) + 1)
}
}
}
return Array.from(counts.entries())
@@ -54,7 +53,7 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
let results = integrations
if (activeCategory) {
results = results.filter((i) => i.integrationType === activeCategory)
results = results.filter((i) => i.integrationTypes?.includes(activeCategory))
}
const q = query.trim().toLowerCase()
@@ -75,7 +74,7 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
return (
<div>
<div className='mb-6 flex flex-col gap-4 sm:flex-row sm:items-center'>
<div className='mb-6 flex flex-col gap-4 px-6 sm:flex-row sm:items-center'>
<div className='relative max-w-[480px] flex-1'>
<svg
aria-hidden='true'
@@ -99,14 +98,14 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
</div>
</div>
<div className='mb-8 flex flex-wrap gap-2'>
<div className='mb-6 flex flex-wrap gap-2 px-6'>
<button
type='button'
onClick={() => setActiveCategory(null)}
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
activeCategory === null
? 'border-[#555] bg-[#333] text-[var(--landing-text)]'
: 'border-[var(--landing-border)] bg-transparent text-[var(--landing-text-muted)] hover:border-[var(--landing-border-strong)] hover:text-[var(--landing-text)]'
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
}`}
>
All
@@ -116,10 +115,10 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
key={cat}
type='button'
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
activeCategory === cat
? 'border-[#555] bg-[#333] text-[var(--landing-text)]'
: 'border-[var(--landing-border)] bg-transparent text-[var(--landing-text-muted)] hover:border-[var(--landing-border-strong)] hover:text-[var(--landing-text)]'
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
}`}
>
{CATEGORY_LABELS[cat] || cat}
@@ -127,16 +126,18 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
))}
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{filtered.length === 0 ? (
<p className='py-12 text-center text-[#555] text-[15px]'>
<p className='py-12 text-center text-[15px] text-[var(--landing-text-subtle)]'>
No integrations found
{query ? <> for &ldquo;{query}&rdquo;</> : null}
{activeCategory ? <> in {CATEGORY_LABELS[activeCategory] || activeCategory}</> : null}
</p>
) : (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
<div>
{filtered.map((integration) => (
<IntegrationCard
<IntegrationRow
key={integration.type}
integration={integration}
IconComponent={blockTypeToIconMap[integration.type]}
@@ -41,9 +41,7 @@ export function IntegrationIcon({
{Icon ? (
<Icon className={cn(iconClassName, 'text-white')} />
) : (
<span className={cn('font-[500] text-white leading-none', fallbackClassName)}>
{name.charAt(0)}
</span>
<span className={cn('text-white leading-none', fallbackClassName)}>{name.charAt(0)}</span>
)}
</Tag>
)
File diff suppressed because it is too large Load Diff
@@ -34,6 +34,6 @@ export interface Integration {
triggerCount: number
authType: AuthType
category: string
integrationType?: string
integrationTypes?: string[]
tags?: string[]
}
+65 -38
View File
@@ -1,5 +1,7 @@
import type { Metadata } from 'next'
import { Badge } from '@/components/emcn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { IntegrationCard } from './components/integration-card'
import { IntegrationGrid } from './components/integration-grid'
import { RequestIntegrationModal } from './components/request-integration-modal'
import { blockTypeToIconMap } from './data/icon-mapping'
@@ -18,6 +20,14 @@ const TOP_NAMES = [...new Set(POPULAR_WORKFLOWS.flatMap((p) => [p.from, p.to]))]
const baseUrl = getBaseUrl()
/** Curated featured integrations — high-recognition services shown as cards. */
const FEATURED_SLUGS = ['slack', 'notion', 'github', 'gmail'] as const
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter(
(i): i is Integration => i !== undefined
)
export const metadata: Metadata = {
title: 'Integrations',
description: `Connect ${INTEGRATION_COUNT}+ apps and services with Sim's AI workflow automation. Build intelligent pipelines with ${TOP_NAMES.join(', ')}, and more.`,
@@ -90,7 +100,7 @@ export default function IntegrationsPage() {
}
return (
<>
<section className='bg-[var(--landing-bg)]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
@@ -100,64 +110,81 @@ export default function IntegrationsPage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListJsonLd) }}
/>
<div className='mx-auto max-w-[1200px] px-6 py-16 sm:px-8 md:px-12'>
{/* Hero */}
<section aria-labelledby='integrations-heading' className='mb-16'>
{/* Hero */}
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<Badge
variant='blue'
size='md'
dot
className='mb-5 bg-white/10 font-season text-white uppercase tracking-[0.02em]'
>
Integrations
</Badge>
<div className='flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between'>
<h1
id='integrations-heading'
className='mb-4 text-balance font-[500] text-[40px] text-[var(--landing-text)] leading-tight sm:text-[56px]'
className='text-balance text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
Integrations
</h1>
<p className='max-w-[640px] text-[18px] text-[var(--landing-text-muted)] leading-relaxed'>
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
Connect every tool your team uses. Build AI-powered workflows that automate tasks across{' '}
{TOP_NAMES.slice(0, 4).map((name, i, arr) => {
const integration = allIntegrations.find((int) => int.name === name)
const Icon = integration ? blockTypeToIconMap[integration.type] : undefined
return (
<span key={name} className='inline-flex items-center gap-[5px]'>
{Icon && (
<span
aria-hidden='true'
className='inline-flex shrink-0'
style={{ opacity: 0.65 }}
>
<Icon className='h-[0.85em] w-[0.85em]' />
</span>
)}
{name}
{i < arr.length - 1 ? ', ' : ''}
</span>
)
})}
{' and more.'}
{INTEGRATION_COUNT} apps and services.
</p>
</section>
</div>
</div>
{/* Searchable grid — client component */}
{/* Full-width divider */}
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
{/* Border-railed content */}
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
{/* Featured integrations — top */}
{featured.length > 0 && (
<>
<nav aria-label='Featured integrations' className='flex flex-col sm:flex-row'>
{featured.map((integration) => (
<IntegrationCard
key={integration.type}
integration={integration}
IconComponent={blockTypeToIconMap[integration.type]}
/>
))}
</nav>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
{/* All Integrations — search, filters, rows */}
<section aria-labelledby='all-integrations-heading'>
<h2
id='all-integrations-heading'
className='mb-8 font-[500] text-[24px] text-[var(--landing-text)]'
>
All Integrations
</h2>
<div className='px-6 pt-10 pb-4'>
<h2
id='all-integrations-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
All Integrations
</h2>
</div>
<IntegrationGrid integrations={allIntegrations} />
</section>
{/* Integration request */}
<div className='mt-16 flex flex-col items-start gap-3 border-[var(--landing-border)] border-t pt-10 sm:flex-row sm:items-center sm:justify-between'>
<div className='flex flex-col items-start gap-3 px-6 py-6 sm:flex-row sm:items-center sm:justify-between'>
<div>
<p className='font-[500] text-[15px] text-[var(--landing-text)]'>
<p className='text-[15px] text-white tracking-[-0.02em]'>
Don&apos;t see the integration you need?
</p>
<p className='mt-0.5 text-[#555] text-[13px]'>
<p className='mt-0.5 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
Let us know and we&apos;ll prioritize it.
</p>
</div>
<RequestIntegrationModal />
</div>
</div>
</>
{/* Closing full-width divider */}
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
</section>
)
}
@@ -3,14 +3,7 @@ import Link from 'next/link'
import { notFound } from 'next/navigation'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import {
Breadcrumbs,
CapabilityTags,
DetailItem,
ModelCard,
ProviderIcon,
StatCard,
} from '@/app/(landing)/models/components/model-primitives'
import { FeaturedModelCard, ProviderIcon } from '@/app/(landing)/models/components/model-primitives'
import {
ALL_CATALOG_MODELS,
buildModelCapabilityFacts,
@@ -165,66 +158,88 @@ export default async function ModelPage({
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className='mx-auto max-w-[1280px] px-6 py-12 sm:px-8 md:px-12'>
<Breadcrumbs
items={[
{ label: 'Home', href: '/' },
{ label: 'Models', href: '/models' },
{ label: provider.name, href: provider.href },
{ label: model.displayName },
]}
/>
<section className='bg-[var(--landing-bg)]'>
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<div className='mb-6'>
<Link
href={provider.href}
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
<svg
className='h-3 w-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<line
x1='1'
y1='5'
x2='10'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
/>
<path
d='M6.5 2L3.5 5L6.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
/>
</svg>
Back to {provider.name}
</Link>
</div>
<section aria-labelledby='model-heading' className='mb-14'>
<div className='mb-6 flex items-start gap-4'>
<div className='mb-6 flex items-center gap-5'>
<ProviderIcon
provider={provider}
className='h-16 w-16 rounded-3xl'
className='h-16 w-16 rounded-[5px]'
iconClassName='h-8 w-8'
/>
<div className='min-w-0'>
<p className='text-[12px] text-[var(--landing-text-muted)] uppercase tracking-[0.12em]'>
<div>
<p className='mb-0.5 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.name} model
</p>
<h1
id='model-heading'
className='font-[500] text-[38px] text-[var(--landing-text)] leading-tight sm:text-[48px]'
className='text-[28px] text-white leading-[100%] tracking-[-0.02em] sm:text-[36px] lg:text-[44px]'
>
{model.displayName}
</h1>
<p className='mt-2 break-all text-[13px] text-[var(--landing-text-muted)]'>
Model ID: {model.id}
</p>
</div>
</div>
<p className='max-w-[820px] text-[17px] text-[var(--landing-text-muted)] leading-relaxed'>
<p className='mb-8 max-w-[700px] text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em]'>
{model.summary}
{model.bestFor ? ` ${model.bestFor}` : ''}
</p>
<div className='mt-8 flex flex-wrap gap-3'>
<Link
href={provider.href}
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--landing-border-strong)] px-3 font-[430] text-[14px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
Explore {provider.name} models
</Link>
<div className='flex flex-wrap gap-2'>
<a
href='https://sim.ai'
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--white)] bg-[var(--white)] px-3 font-[430] text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Build with this model
</a>
<Link
href={provider.href}
className='inline-flex h-[32px] items-center rounded-[5px] border border-[var(--landing-border-strong)] px-2.5 font-season text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
All {provider.name} models
</Link>
</div>
</section>
</div>
<section
aria-label='Model stats'
className='mb-16 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'
>
<StatCard label='Input price' value={`${formatPrice(model.pricing.input)}/1M`} />
<StatCard
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
<InfoRow label='Input price' value={`${formatPrice(model.pricing.input)}/1M`} />
<InfoRow
label='Cached input'
value={
model.pricing.cachedInput !== undefined
@@ -232,158 +247,72 @@ export default async function ModelPage({
: 'N/A'
}
/>
<StatCard label='Output price' value={`${formatPrice(model.pricing.output)}/1M`} />
<StatCard
<InfoRow label='Output price' value={`${formatPrice(model.pricing.output)}/1M`} />
<InfoRow
label='Context window'
value={model.contextWindow ? formatTokenCount(model.contextWindow) : 'Unknown'}
/>
</section>
<InfoRow
label='Max output'
value={
model.capabilities.maxOutputTokens
? `${formatTokenCount(getEffectiveMaxOutputTokens(model.capabilities))} tokens`
: 'Not published'
}
/>
<InfoRow label='Provider' value={provider.name} />
<InfoRow label='Updated' value={formatUpdatedAt(model.pricing.updatedAt)} />
{model.bestFor ? <InfoRow label='Best for' value={model.bestFor} /> : null}
<div className='grid grid-cols-1 gap-16 lg:grid-cols-[1fr_320px]'>
<div className='min-w-0 space-y-16'>
<section aria-labelledby='pricing-heading'>
<h2
id='pricing-heading'
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
>
Pricing and limits
</h2>
<p className='mb-6 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
Pricing below is generated directly from the provider registry in Sim. All amounts
are listed per one million tokens.
</p>
{capabilityFacts.length > 0 && (
<>
{capabilityFacts.map((item) => (
<InfoRow key={item.label} label={item.label} value={item.value} />
))}
</>
)}
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
<DetailItem label='Input price' value={`${formatPrice(model.pricing.input)}/1M`} />
<DetailItem
label='Cached input'
value={
model.pricing.cachedInput !== undefined
? `${formatPrice(model.pricing.cachedInput)}/1M`
: 'N/A'
}
/>
<DetailItem
label='Output price'
value={`${formatPrice(model.pricing.output)}/1M`}
/>
<DetailItem label='Updated' value={formatUpdatedAt(model.pricing.updatedAt)} />
<DetailItem
label='Context window'
value={
model.contextWindow
? `${formatTokenCount(model.contextWindow)} tokens`
: 'Unknown'
}
/>
<DetailItem
label='Max output'
value={
model.capabilities.maxOutputTokens
? `${formatTokenCount(getEffectiveMaxOutputTokens(model.capabilities))} tokens`
: 'Not published'
}
/>
<DetailItem label='Provider' value={provider.name} />
{model.bestFor ? <DetailItem label='Best for' value={model.bestFor} /> : null}
</div>
</section>
<section aria-labelledby='capabilities-heading'>
<h2
id='capabilities-heading'
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
>
Capabilities
</h2>
<p className='mb-6 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
These capability flags are generated from the provider and model definitions tracked
in Sim.
</p>
<CapabilityTags tags={model.capabilityTags} />
<div className='mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
{capabilityFacts.map((item) => (
<DetailItem key={item.label} label={item.label} value={item.value} />
{relatedModels.length > 0 && (
<>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<nav aria-label='Related models' className='flex flex-col sm:flex-row'>
{relatedModels.slice(0, 3).map((entry) => (
<FeaturedModelCard key={entry.id} provider={provider} model={entry} />
))}
</div>
</section>
</nav>
</>
)}
{relatedModels.length > 0 && (
<section aria-labelledby='related-models-heading'>
<h2
id='related-models-heading'
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
>
Related {provider.name} models
</h2>
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
Browse comparable models from the same provider to compare pricing, context
window, and capability coverage.
</p>
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
{relatedModels.map((entry) => (
<ModelCard key={entry.id} provider={provider} model={entry} />
))}
</div>
</section>
)}
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<section
aria-labelledby='model-faq-heading'
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
<section aria-labelledby='model-faq-heading' className='px-6 py-10'>
<h2
id='model-faq-heading'
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
<h2
id='model-faq-heading'
className='font-[500] text-[28px] text-[var(--landing-text)]'
>
Frequently asked questions
</h2>
<div className='mt-3'>
<LandingFAQ faqs={faqs} />
</div>
</section>
</div>
<aside className='space-y-5' aria-label='Model details'>
<div className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5'>
<h2 className='mb-4 font-[500] text-[16px] text-[var(--landing-text)]'>
Quick details
</h2>
<div className='space-y-3'>
<DetailItem label='Display name' value={model.displayName} />
<DetailItem label='Provider' value={provider.name} />
<DetailItem
label='Context tracked'
value={model.contextWindow ? 'Yes' : 'Partial'}
/>
<DetailItem
label='Pricing updated'
value={formatUpdatedAt(model.pricing.updatedAt)}
/>
</div>
Frequently asked questions
</h2>
<div>
<LandingFAQ faqs={faqs} />
</div>
<div className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-5'>
<h2 className='mb-4 font-[500] text-[16px] text-[var(--landing-text)]'>
Browse more
</h2>
<div className='space-y-2'>
<Link
href={provider.href}
className='block rounded-xl px-3 py-2 text-[14px] text-[var(--landing-text-muted)] transition-colors hover:bg-[var(--landing-bg-elevated)] hover:text-[var(--landing-text)]'
>
All {provider.name} models
</Link>
<Link
href='/models'
className='block rounded-xl px-3 py-2 text-[14px] text-[var(--landing-text-muted)] transition-colors hover:bg-[var(--landing-bg-elevated)] hover:text-[var(--landing-text)]'
>
Full models directory
</Link>
</div>
</div>
</aside>
</section>
</div>
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
</section>
</>
)
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='flex items-baseline justify-between gap-4 px-6 py-4'>
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{label}
</span>
<span className='text-right text-[14px] text-white leading-snug'>{value}</span>
</div>
</>
)
+129 -121
View File
@@ -1,19 +1,21 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { Badge } from '@/components/emcn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import {
Breadcrumbs,
CapabilityTags,
ModelCard,
ProviderCard,
ChevronArrow,
FeaturedModelCard,
FeaturedProviderCard,
ProviderIcon,
StatCard,
} from '@/app/(landing)/models/components/model-primitives'
import { ModelTimelineChart } from '@/app/(landing)/models/components/model-timeline-chart'
import {
buildProviderFaqs,
formatPrice,
formatTokenCount,
getProviderBySlug,
getProviderCapabilitySummary,
MODEL_PROVIDERS_WITH_CATALOGS,
TOP_MODEL_PROVIDERS,
} from '@/app/(landing)/models/utils'
@@ -95,7 +97,6 @@ export default async function ProviderModelsPage({
}
const faqs = buildProviderFaqs(provider)
const capabilitySummary = getProviderCapabilitySummary(provider)
const relatedProviders = MODEL_PROVIDERS_WITH_CATALOGS.filter(
(entry) => entry.id !== provider.id && TOP_MODEL_PROVIDERS.includes(entry.name)
).slice(0, 4)
@@ -153,142 +154,149 @@ export default async function ProviderModelsPage({
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className='mx-auto max-w-[1280px] px-6 py-12 sm:px-8 md:px-12'>
<Breadcrumbs
items={[
{ label: 'Home', href: '/' },
{ label: 'Models', href: '/models' },
{ label: provider.name },
]}
/>
<section className='bg-[var(--landing-bg)]'>
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<div className='mb-6'>
<Link
href='/models'
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
>
<svg
className='h-3 w-3 shrink-0'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<line
x1='1'
y1='5'
x2='10'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
/>
<path
d='M6.5 2L3.5 5L6.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
/>
</svg>
Back to Models
</Link>
</div>
<section aria-labelledby='provider-heading' className='mb-14'>
<div className='mb-6 flex items-center gap-4'>
<ProviderIcon
provider={provider}
className='h-16 w-16 rounded-3xl'
iconClassName='h-8 w-8'
/>
<div>
<p className='text-[12px] text-[var(--landing-text-muted)] uppercase tracking-[0.12em]'>
Provider
</p>
<Badge
variant='blue'
size='md'
dot
className='mb-5 bg-white/10 font-season text-white uppercase tracking-[0.02em]'
>
Provider
</Badge>
<div className='flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between'>
<div className='flex items-center gap-4'>
<ProviderIcon
provider={provider}
className='h-12 w-12 rounded-[5px]'
iconClassName='h-6 w-6'
/>
<h1
id='provider-heading'
className='font-[500] text-[38px] text-[var(--landing-text)] leading-tight sm:text-[48px]'
className='font-[430] font-season text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
{provider.name} models
</h1>
</div>
<span className='shrink-0 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.modelCount} models
</span>
</div>
</div>
<p className='max-w-[820px] text-[17px] text-[var(--landing-text-muted)] leading-relaxed'>
{provider.summary} Browse every {provider.name} model page generated from Sim&apos;s
provider registry with human-readable names, pricing, context windows, and capability
metadata.
</p>
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
<StatCard label='Models tracked' value={provider.modelCount.toString()} />
<StatCard
label='Default model'
value={provider.defaultModelDisplayName || 'Dynamic'}
compact
/>
<StatCard
label='Metadata coverage'
value={provider.contextInformationAvailable ? 'Tracked' : 'Partial'}
compact
/>
<StatCard
label='Featured models'
value={provider.featuredModels.length.toString()}
compact
/>
</div>
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
{provider.featuredModels.length > 0 && (
<>
<nav aria-label='Featured models' className='flex flex-col sm:flex-row'>
{provider.featuredModels.slice(0, 3).map((model) => (
<FeaturedModelCard key={model.id} provider={provider} model={model} />
))}
</nav>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
<div className='mt-6'>
<CapabilityTags tags={provider.providerCapabilityTags} />
</div>
</section>
<ModelTimelineChart models={provider.models} providerId={provider.id} />
<section aria-labelledby='provider-models-heading' className='mb-16'>
<h2
id='provider-models-heading'
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
>
All {provider.name} models
</h2>
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
Every model below links to a dedicated SEO page with exact pricing, context window,
capability support, and related model recommendations.
</p>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
{provider.models.map((model) => (
<ModelCard key={model.id} provider={provider} model={model} />
))}
</div>
</section>
{provider.models.map((model) => (
<div key={model.id}>
<Link
href={model.href}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
{model.displayName}
</h3>
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
{model.id}
</p>
</div>
<span className='hidden shrink-0 font-martian-mono text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.1em] md:block'>
{formatPrice(model.pricing.input)}/1M in
</span>
<span className='hidden shrink-0 font-martian-mono text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.1em] md:block'>
{formatPrice(model.pricing.output)}/1M out
</span>
{model.contextWindow ? (
<span className='hidden shrink-0 font-martian-mono text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.1em] lg:block'>
{formatTokenCount(model.contextWindow)} ctx
</span>
) : null}
<ChevronArrow />
</Link>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</div>
))}
<section
aria-labelledby='lineup-snapshot-heading'
className='mb-16 rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
>
<h2
id='lineup-snapshot-heading'
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
>
Lineup snapshot
</h2>
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
A quick view of the strongest differentiators in the {provider.name} model lineup based
on the metadata currently tracked in Sim.
</p>
{relatedProviders.length > 0 && (
<>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<nav aria-label='Related providers' className='flex flex-col sm:flex-row'>
{relatedProviders.map((entry) => (
<FeaturedProviderCard key={entry.id} provider={entry} />
))}
</nav>
</>
)}
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
{capabilitySummary.map((item) => (
<StatCard key={item.label} label={item.label} value={item.value} compact />
))}
</div>
</section>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{relatedProviders.length > 0 && (
<section aria-labelledby='related-providers-heading' className='mb-16'>
<section aria-labelledby='provider-faq-heading' className='px-6 py-10'>
<h2
id='related-providers-heading'
className='mb-2 font-[500] text-[28px] text-[var(--landing-text)]'
id='provider-faq-heading'
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
Compare with other providers
Frequently asked questions
</h2>
<p className='mb-8 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
Explore similar provider hubs to compare model lineups, pricing surfaces, and
long-context coverage across the broader AI ecosystem.
</p>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
{relatedProviders.map((entry) => (
<ProviderCard key={entry.id} provider={entry} />
))}
<div>
<LandingFAQ faqs={faqs} />
</div>
</section>
)}
</div>
<section
aria-labelledby='provider-faq-heading'
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
>
<h2
id='provider-faq-heading'
className='font-[500] text-[28px] text-[var(--landing-text)]'
>
Frequently asked questions
</h2>
<div className='mt-3'>
<LandingFAQ faqs={faqs} />
</div>
</section>
</div>
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
</section>
</>
)
}
@@ -0,0 +1,9 @@
import { MODEL_CATALOG_PROVIDERS } from '@/app/(landing)/models/utils'
const colorMap = new Map(
MODEL_CATALOG_PROVIDERS.filter((p) => p.color).map((p) => [p.id, p.color as string])
)
export function getProviderColor(providerId: string): string {
return colorMap.get(providerId) ?? '#888888'
}
@@ -0,0 +1,245 @@
'use client'
import type { ComponentType } from 'react'
import { useMemo } from 'react'
import Link from 'next/link'
import { getProviderColor } from '@/app/(landing)/models/components/consts'
import type { CatalogModel } from '@/app/(landing)/models/utils'
import {
formatPrice,
formatTokenCount,
MODEL_CATALOG_PROVIDERS,
} from '@/app/(landing)/models/utils'
/** Providers that host other providers' models — deprioritized to avoid duplicates. */
const RESELLER_PROVIDERS = new Set(
MODEL_CATALOG_PROVIDERS.filter((p) => p.isReseller).map((p) => p.id)
)
const PROVIDER_ICON_MAP: Record<string, ComponentType<{ className?: string }>> = (() => {
const map: Record<string, ComponentType<{ className?: string }>> = {}
for (const provider of MODEL_CATALOG_PROVIDERS) {
if (provider.icon) {
map[provider.id] = provider.icon
}
}
return map
})()
function selectComparisonModels(models: CatalogModel[]): CatalogModel[] {
const seen = new Set<string>()
const result: CatalogModel[] = []
const sorted = [...models].sort((a, b) => {
const score = (m: CatalogModel) => {
const reseller = RESELLER_PROVIDERS.has(m.providerId) ? -50 : 0
const reasoning = m.capabilities.reasoningEffort || m.capabilities.thinking ? 10 : 0
const context = (m.contextWindow ?? 0) / 100000
return reseller + reasoning + context
}
return score(b) - score(a)
})
for (const model of sorted) {
if (result.length >= 10) break
const nameKey = model.displayName.toLowerCase()
if (seen.has(nameKey)) continue
seen.add(nameKey)
result.push(model)
}
return result
}
interface ModelLabelProps {
model: CatalogModel
}
function ModelLabel({ model }: ModelLabelProps) {
const Icon = PROVIDER_ICON_MAP[model.providerId]
return (
<div className='flex w-[140px] shrink-0 items-center justify-end gap-1.5 sm:w-[180px]'>
{Icon && <Icon className='h-3.5 w-3.5 shrink-0' />}
<span className='truncate font-medium text-[13px] text-[var(--landing-text)] leading-none tracking-[-0.01em]'>
{model.displayName}
</span>
</div>
)
}
interface ChartProps {
models: CatalogModel[]
}
function StackedCostChart({ models }: ChartProps) {
const data = useMemo(() => {
const entries = models
.map((model) => ({
model,
input: model.pricing.input,
output: model.pricing.output,
total: model.pricing.input + model.pricing.output,
}))
.filter((e) => e.total > 0)
.sort((a, b) => a.total - b.total)
const maxTotal = entries.length > 0 ? Math.max(...entries.map((e) => e.total)) : 0
return { entries, maxTotal }
}, [models])
if (data.entries.length === 0) return null
return (
<div className='flex flex-col gap-3'>
<div className='flex flex-col gap-1'>
<h3 className='text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'>
Cost
</h3>
<span className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
Per 1M tokens
</span>
</div>
<div className='flex flex-col gap-1.5'>
{data.entries.map(({ model, input, output, total }) => {
const totalPct = data.maxTotal > 0 ? (total / data.maxTotal) * 100 : 0
const inputPct = total > 0 ? (input / total) * 100 : 0
const color = getProviderColor(model.providerId)
return (
<Link
key={model.id}
href={model.href}
className='-mx-2 flex items-center gap-3 rounded-md px-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<ModelLabel model={model} />
<div className='relative flex h-7 min-w-0 flex-1 items-center'>
<div
className='flex h-full overflow-hidden rounded-r-[3px]'
style={{ width: `${Math.max(totalPct, 3)}%` }}
>
<div
className='h-full'
style={{
width: `${inputPct}%`,
backgroundColor: color,
opacity: 0.8,
}}
/>
<div
className='h-full'
style={{
width: `${100 - inputPct}%`,
backgroundColor: color,
opacity: 0.35,
}}
/>
</div>
<span className='ml-2.5 shrink-0 font-mono text-[var(--landing-text-muted)] text-xs'>
{formatPrice(input)} input / {formatPrice(output)} output
</span>
</div>
</Link>
)
})}
</div>
</div>
)
}
function ContextWindowChart({ models }: ChartProps) {
const data = useMemo(() => {
const entries = models
.map((model) => ({
model,
value: model.contextWindow,
}))
.filter((e): e is { model: CatalogModel; value: number } => e.value !== null && e.value > 0)
.sort((a, b) => a.value - b.value)
const maxValue = entries.length > 0 ? Math.max(...entries.map((e) => e.value)) : 0
return { entries, maxValue }
}, [models])
if (data.entries.length === 0) return null
return (
<div className='flex flex-col gap-3'>
<div className='flex flex-col gap-1'>
<h3 className='text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'>
Context window
</h3>
<span className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
Max tokens
</span>
</div>
<div className='flex flex-col gap-1.5'>
{data.entries.map(({ model, value }) => {
const pct = data.maxValue > 0 ? (value / data.maxValue) * 100 : 0
const color = getProviderColor(model.providerId)
return (
<Link
key={model.id}
href={model.href}
className='-mx-2 flex items-center gap-3 rounded-md px-2 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<ModelLabel model={model} />
<div className='relative flex h-7 min-w-0 flex-1 items-center'>
<div
className='h-full rounded-r-[3px]'
style={{
width: `${Math.max(pct, 3)}%`,
backgroundColor: color,
opacity: 0.8,
}}
/>
<span className='ml-2.5 shrink-0 font-mono text-[var(--landing-text-muted)] text-xs'>
{formatTokenCount(value)}
</span>
</div>
</Link>
)
})}
</div>
</div>
)
}
interface ModelComparisonChartsProps {
models: CatalogModel[]
}
export function ModelComparisonCharts({ models }: ModelComparisonChartsProps) {
const comparisonModels = useMemo(() => selectComparisonModels(models), [models])
return (
<section aria-labelledby='comparison-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='comparison-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
Compare models
</h2>
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
Side-by-side comparison of top models across key metrics.
</p>
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='flex flex-col sm:flex-row'>
<div className='flex-1 p-6'>
<StackedCostChart models={comparisonModels} />
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)] sm:h-auto sm:w-px' />
<div className='flex-1 p-6'>
<ContextWindowChart models={comparisonModels} />
</div>
</div>
</section>
)
}
@@ -3,20 +3,14 @@
import { useMemo, useState } from 'react'
import Link from 'next/link'
import { Input } from '@/components/emcn'
import { SearchIcon } from '@/components/icons'
import { cn } from '@/lib/core/utils/cn'
import {
CapabilityTags,
DetailItem,
ModelCard,
ProviderIcon,
StatCard,
} from '@/app/(landing)/models/components/model-primitives'
import { ChevronArrow, ProviderIcon } from '@/app/(landing)/models/components/model-primitives'
import {
type CatalogModel,
type CatalogProvider,
formatPrice,
formatTokenCount,
MODEL_PROVIDERS_WITH_CATALOGS,
MODEL_PROVIDERS_WITH_DYNAMIC_CATALOGS,
TOTAL_MODELS,
} from '@/app/(landing)/models/utils'
export function ModelDirectory() {
@@ -35,7 +29,7 @@ export function ModelDirectory() {
const normalizedQuery = query.trim().toLowerCase()
const { filteredProviders, filteredDynamicProviders, visibleModelCount } = useMemo(() => {
const { filteredProviders, filteredDynamicProviders } = useMemo(() => {
const filteredProviders = MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => {
const providerMatchesSearch =
normalizedQuery.length > 0 && provider.searchText.includes(normalizedQuery)
@@ -77,15 +71,9 @@ export function ModelDirectory() {
return provider.searchText.includes(normalizedQuery)
})
const visibleModelCount = filteredProviders.reduce(
(count, provider) => count + provider.models.length,
0
)
return {
filteredProviders,
filteredDynamicProviders,
visibleModelCount,
}
}, [activeProviderId, normalizedQuery])
@@ -93,170 +81,143 @@ export function ModelDirectory() {
return (
<div>
<div className='mb-8 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between'>
<div className='relative max-w-[560px] flex-1'>
<SearchIcon
<div className='mb-6 flex flex-col gap-4 px-6 sm:flex-row sm:items-center'>
<div className='relative max-w-[480px] flex-1'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[var(--landing-text-muted)]'
/>
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<circle cx={11} cy={11} r={8} />
<path d='m21 21-4.35-4.35' />
</svg>
<Input
type='search'
placeholder='Search models, providers, capabilities, or pricing details'
placeholder='Search models, providers, or capabilities…'
value={query}
onChange={(event) => setQuery(event.target.value)}
className='h-11 border-[var(--landing-border)] bg-[var(--landing-bg-card)] pl-10 text-[var(--landing-text)] placeholder:text-[var(--landing-text-muted)]'
className='pl-9'
aria-label='Search AI models'
/>
</div>
<p className='text-[13px] text-[var(--landing-text-muted)] leading-relaxed'>
Showing {visibleModelCount.toLocaleString('en-US')} of{' '}
{TOTAL_MODELS.toLocaleString('en-US')} models
{activeProviderId ? ' in one provider' : ''}.
</p>
</div>
<div className='mb-10 flex flex-wrap gap-2'>
<FilterButton
isActive={activeProviderId === null}
<div className='mb-6 flex flex-wrap gap-2 px-6'>
<button
type='button'
onClick={() => setActiveProviderId(null)}
label={`All providers (${MODEL_PROVIDERS_WITH_CATALOGS.length})`}
/>
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
activeProviderId === null
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
}`}
>
All
</button>
{providerOptions.map((provider) => (
<FilterButton
<button
key={provider.id}
isActive={activeProviderId === provider.id}
type='button'
onClick={() =>
setActiveProviderId(activeProviderId === provider.id ? null : provider.id)
}
label={`${provider.name} (${provider.count})`}
/>
className={`rounded-[5px] border px-[9px] py-0.5 text-[13.5px] transition-colors ${
activeProviderId === provider.id
? 'border-[var(--landing-border-strong)] bg-[var(--landing-bg-elevated)] text-[var(--landing-text)]'
: 'border-[var(--landing-border-strong)] text-[var(--landing-text)] hover:bg-[var(--landing-bg-elevated)]'
}`}
>
{provider.name}
</button>
))}
</div>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
{!hasResults ? (
<div className='rounded-2xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-6 py-12 text-center'>
<h3 className='font-[500] text-[18px] text-[var(--landing-text)]'>No matches found</h3>
<p className='mt-2 text-[14px] text-[var(--landing-text-muted)] leading-relaxed'>
<div className='px-6 py-12 text-center'>
<h3 className='text-[18px] text-white'>No matches found</h3>
<p className='mt-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
Try a provider name like OpenAI or Anthropic, or search for capabilities like
&nbsp;structured outputs, reasoning, or deep research.
</p>
</div>
) : (
<div className='space-y-10'>
{filteredProviders.map((provider) => (
<section
key={provider.id}
aria-labelledby={`${provider.id}-heading`}
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
>
<div className='mb-6 flex flex-col gap-5 border-[var(--landing-border)] border-b pb-6 lg:flex-row lg:items-start lg:justify-between'>
<div className='min-w-0'>
<div className='mb-3 flex items-center gap-3'>
<ProviderIcon provider={provider} />
<div>
<p className='text-[12px] text-[var(--landing-text-muted)]'>Provider</p>
<h2
id={`${provider.id}-heading`}
className='font-[500] text-[24px] text-[var(--landing-text)]'
>
{provider.name}
</h2>
</div>
</div>
<div>
{filteredProviders.map((provider, index) => (
<section key={provider.id} aria-labelledby={`${provider.id}-heading`}>
{index > 0 && <div className='h-px w-full bg-[var(--landing-bg-elevated)]' />}
<p className='max-w-[720px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
{provider.description}
</p>
<Link
href={provider.href}
className='mt-3 inline-flex text-[#555] text-[13px] transition-colors hover:text-[var(--landing-text-muted)]'
<Link
href={provider.href}
className='group/link flex items-center gap-3 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<ProviderIcon
provider={provider}
className='h-8 w-8 rounded-[5px]'
iconClassName='h-4 w-4'
/>
<div className='min-w-0 flex-1'>
<h2
id={`${provider.id}-heading`}
className='text-[14px] text-white leading-snug tracking-[-0.02em]'
>
View provider page →
</Link>
{provider.name}
</h2>
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
{provider.modelCount} models &middot; {provider.description}
</p>
</div>
<ChevronArrow />
</Link>
<div className='grid shrink-0 grid-cols-2 gap-3 sm:grid-cols-3'>
<StatCard label='Models' value={provider.models.length.toString()} />
<StatCard
label='Default'
value={provider.defaultModelDisplayName || 'Dynamic'}
compact
/>
<StatCard
label='Context info'
value={provider.contextInformationAvailable ? 'Tracked' : 'Limited'}
compact
/>
</div>
</div>
<div className='mb-6'>
<CapabilityTags tags={provider.providerCapabilityTags} />
</div>
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
{provider.models.map((model) => (
<ModelCard key={model.id} provider={provider} model={model} />
))}
</div>
{provider.models.map((model) => (
<ModelRow key={model.id} provider={provider} model={model} />
))}
</section>
))}
{filteredDynamicProviders.length > 0 && (
<section
aria-labelledby='dynamic-catalogs-heading'
className='rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
>
<div className='mb-6'>
<section aria-labelledby='dynamic-catalogs-heading'>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<div className='px-6 pt-8 pb-6'>
<h2
id='dynamic-catalogs-heading'
className='font-[500] text-[24px] text-[var(--landing-text)]'
className='text-[18px] text-white leading-[100%] tracking-[-0.02em] lg:text-[20px]'
>
Dynamic model catalogs
</h2>
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
These providers are supported by Sim, but their model lists are loaded dynamically
at runtime rather than hard-coded into the public catalog.
<p className='mt-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
These providers load their model lists dynamically at runtime.
</p>
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<nav aria-label='Dynamic catalog providers' className='flex flex-col lg:flex-row'>
{filteredDynamicProviders.map((provider) => (
<article
<div
key={provider.id}
className='rounded-2xl border border-[var(--landing-border)] bg-[var(--landing-bg-elevated)] p-5'
className='flex flex-1 items-center gap-3 border-[var(--landing-bg-elevated)] border-t px-6 py-4 first:border-t-0 lg:border-t-0 lg:border-l lg:first:border-l-0'
>
<div className='mb-4 flex items-center gap-3'>
<ProviderIcon provider={provider} />
<div className='min-w-0'>
<h3 className='font-[500] text-[16px] text-[var(--landing-text)]'>
{provider.name}
</h3>
<p className='text-[12px] text-[var(--landing-text-muted)]'>
{provider.id}
</p>
</div>
<ProviderIcon
provider={provider}
className='h-8 w-8 rounded-[5px]'
iconClassName='h-4 w-4'
/>
<div className='min-w-0 flex-1'>
<h3 className='text-[14px] text-white leading-snug'>{provider.name}</h3>
<p className='line-clamp-1 text-[12px] text-[var(--landing-text-muted)] leading-[150%]'>
{provider.description}
</p>
</div>
<p className='text-[13px] text-[var(--landing-text-muted)] leading-relaxed'>
{provider.description}
</p>
<div className='mt-4 space-y-3 text-[13px]'>
<DetailItem
label='Default'
value={provider.defaultModelDisplayName || 'Selected at runtime'}
/>
<DetailItem label='Catalog source' value='Loaded dynamically inside Sim' />
</div>
<div className='mt-4'>
<CapabilityTags tags={provider.providerCapabilityTags} />
</div>
</article>
</div>
))}
</div>
</nav>
</section>
)}
</div>
@@ -265,27 +226,33 @@ export function ModelDirectory() {
)
}
function FilterButton({
isActive,
onClick,
label,
}: {
isActive: boolean
onClick: () => void
label: string
}) {
function ModelRow({ provider, model }: { provider: CatalogProvider; model: CatalogModel }) {
return (
<button
type='button'
onClick={onClick}
className={cn(
'rounded-full border px-3 py-1.5 text-[12px] transition-colors',
isActive
? 'border-[#555] bg-[#333] text-[var(--landing-text)]'
: 'border-[var(--landing-border)] bg-transparent text-[var(--landing-text-muted)] hover:border-[var(--landing-border-strong)] hover:text-[var(--landing-text)]'
)}
>
{label}
</button>
<>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<Link
href={model.href}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
<ProviderIcon
provider={provider}
className='h-8 w-8 shrink-0 rounded-[5px]'
iconClassName='h-4 w-4'
/>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<h3 className='text-[14px] text-white leading-snug tracking-[-0.02em]'>
{model.displayName}
</h3>
<p className='line-clamp-1 hidden text-[12px] text-[var(--landing-text-muted)] leading-[150%] sm:block'>
{model.id} &middot; Input {formatPrice(model.pricing.input)}/1M &middot; Output{' '}
{formatPrice(model.pricing.output)}/1M
{model.contextWindow ? ` · ${formatTokenCount(model.contextWindow)} context` : ''}
</p>
</div>
<ChevronArrow />
</Link>
</>
)
}
@@ -12,7 +12,7 @@ export function Breadcrumbs({ items }: { items: Array<{ label: string; href?: st
return (
<nav
aria-label='Breadcrumb'
className='mb-10 flex flex-wrap items-center gap-2 text-[#555] text-[13px]'
className='mb-10 flex flex-wrap items-center gap-2 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'
>
{items.map((item, index) => (
<span key={`${item.label}-${index}`} className='inline-flex items-center gap-2'>
@@ -35,7 +35,7 @@ export function Breadcrumbs({ items }: { items: Array<{ label: string; href?: st
export function ProviderIcon({
provider,
className = 'h-12 w-12 rounded-2xl',
className = 'h-12 w-12 rounded-[5px]',
iconClassName = 'h-6 w-6',
}: {
provider: Pick<CatalogProvider, 'icon' | 'name'>
@@ -51,7 +51,7 @@ export function ProviderIcon({
{Icon ? (
<Icon className={iconClassName} />
) : (
<span className='font-[500] text-[14px] text-[var(--landing-text)]'>
<span className='font-[430] text-[14px] text-[var(--landing-text)]'>
{provider.name.slice(0, 2).toUpperCase()}
</span>
)}
@@ -69,12 +69,12 @@ export function StatCard({
compact?: boolean
}) {
return (
<div className='rounded-2xl border border-[var(--landing-border)] bg-[var(--landing-bg-elevated)] px-4 py-3'>
<p className='text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.08em]'>
<div className='rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-elevated)] px-4 py-3'>
<p className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{label}
</p>
<p
className={`mt-1 font-[500] text-[var(--landing-text)] ${
className={`mt-1 font-[430] text-[var(--landing-text)] ${
compact ? 'break-all text-[12px] leading-snug' : 'text-[18px]'
}`}
>
@@ -86,17 +86,49 @@ export function StatCard({
export function DetailItem({ label, value }: { label: string; value: string }) {
return (
<div className='rounded-xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-3 py-2'>
<p className='text-[11px] text-[var(--landing-text-muted)] uppercase tracking-[0.08em]'>
<div className='rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] px-3 py-2'>
<p className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{label}
</p>
<p className='mt-1 break-words font-[500] text-[12px] text-[var(--landing-text)] leading-snug'>
<p className='mt-1 break-words font-[430] text-[12px] text-[var(--landing-text)] leading-snug'>
{value}
</p>
</div>
)
}
export function ChevronArrow() {
return (
<svg
className='h-3 w-3 shrink-0 text-[var(--landing-text-subtle)]'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
aria-hidden='true'
>
<line
x1='0'
y1='5'
x2='9'
y2='5'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
className='origin-left scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
/>
<path
d='M3.5 2L6.5 5L3.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
className='transition-transform duration-200 ease-out group-hover/link:translate-x-[30%]'
/>
</svg>
)
}
export function CapabilityTags({ tags }: { tags: string[] }) {
if (tags.length === 0) {
return null
@@ -116,23 +148,76 @@ export function CapabilityTags({ tags }: { tags: string[] }) {
)
}
export function FeaturedProviderCard({ provider }: { provider: CatalogProvider }) {
return (
<Link
href={provider.href}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<ProviderIcon
provider={provider}
className='h-10 w-10 rounded-[5px]'
iconClassName='h-5 w-5'
/>
<div className='flex flex-col gap-2'>
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{provider.name}</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{provider.description}
</p>
</div>
</Link>
)
}
export function FeaturedModelCard({
provider,
model,
}: {
provider: CatalogProvider
model: CatalogModel
}) {
return (
<Link
href={model.href}
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--landing-bg-elevated)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<ProviderIcon
provider={provider}
className='h-10 w-10 rounded-[5px]'
iconClassName='h-5 w-5'
/>
<div className='flex flex-col gap-2'>
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.name}
</span>
<h3 className='text-lg text-white leading-tight tracking-[-0.01em]'>{model.displayName}</h3>
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{model.summary}
</p>
</div>
</Link>
)
}
export function ProviderCard({ provider }: { provider: CatalogProvider }) {
return (
<Link
href={provider.href}
className='group flex h-full flex-col rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
className='group flex h-full flex-col rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
>
<div className='mb-4 flex items-center gap-3'>
<ProviderIcon provider={provider} />
<div className='min-w-0'>
<h3 className='font-[500] text-[18px] text-[var(--landing-text)]'>{provider.name}</h3>
<p className='text-[12px] text-[var(--landing-text-muted)]'>
<h3 className='font-[430] font-season text-base text-white tracking-[-0.01em]'>
{provider.name}
</h3>
<p className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.modelCount} models tracked
</p>
</div>
</div>
<p className='mb-4 flex-1 text-[14px] text-[var(--landing-text-muted)] leading-relaxed'>
<p className='mb-4 flex-1 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{provider.description}
</p>
@@ -165,26 +250,30 @@ export function ModelCard({
return (
<Link
href={model.href}
className='group flex h-full flex-col rounded-lg border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
className='group flex h-full flex-col rounded-[5px] border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-4 transition-colors hover:border-[var(--landing-border-strong)] hover:bg-[var(--landing-bg-elevated)]'
>
<div className='mb-4 flex items-start gap-3'>
<ProviderIcon
provider={provider}
className='h-10 w-10 rounded-xl'
className='h-10 w-10 rounded-[5px]'
iconClassName='h-5 w-5'
/>
<div className='min-w-0 flex-1'>
{showProvider ? (
<p className='mb-1 text-[12px] text-[var(--landing-text-muted)]'>{provider.name}</p>
<p className='mb-1 font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
{provider.name}
</p>
) : null}
<h3 className='break-all font-[500] text-[16px] text-[var(--landing-text)] leading-snug'>
<h3 className='break-all font-[430] font-season text-base text-white leading-snug tracking-[-0.01em]'>
{model.displayName}
</h3>
<p className='mt-1 break-all text-[12px] text-[var(--landing-text-muted)]'>{model.id}</p>
<p className='mt-1 break-all font-martian-mono text-[var(--landing-text-subtle)] text-xs tracking-[0.1em]'>
{model.id}
</p>
</div>
</div>
<p className='mb-3 line-clamp-3 flex-1 text-[12px] text-[var(--landing-text-muted)] leading-relaxed'>
<p className='mb-3 line-clamp-3 flex-1 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
{model.summary}
</p>
@@ -0,0 +1,132 @@
'use client'
import { useMemo } from 'react'
import Link from 'next/link'
import { getProviderColor } from '@/app/(landing)/models/components/consts'
import type { CatalogModel } from '@/app/(landing)/models/utils'
function formatShortDate(date: string): string {
try {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
}).format(new Date(date))
} catch {
return date
}
}
interface ModelTimelineChartProps {
models: CatalogModel[]
providerId: string
}
const ITEM_WIDTH = 150
export function ModelTimelineChart({ models, providerId }: ModelTimelineChartProps) {
const entries = useMemo(() => {
return models
.filter((m) => m.releaseDate !== null)
.map((m) => ({
model: m,
date: new Date(m.releaseDate as string),
dateStr: m.releaseDate as string,
}))
.sort((a, b) => a.date.getTime() - b.date.getTime())
}, [models])
if (entries.length === 0) return null
const color = getProviderColor(providerId)
return (
<section aria-labelledby='timeline-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='timeline-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
Release timeline
</h2>
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
When each model was first publicly available.
</p>
</div>
<div className='overflow-x-auto px-6 pb-8'>
{/* Fixed height: top labels + line + bottom labels */}
<div
className='relative h-[140px]'
style={{ minWidth: `${entries.length * ITEM_WIDTH}px` }}
>
{/* Horizontal line — vertically centered */}
<div className='absolute top-[70px] right-0 left-0 h-px bg-[var(--landing-border-strong)]' />
{entries.map(({ model, dateStr }, i) => {
const left = i * ITEM_WIDTH + ITEM_WIDTH / 2
const isAbove = i % 2 === 0
return (
<Link
key={model.id}
href={model.href}
className='group absolute flex flex-col items-center'
style={{
left: `${left}px`,
width: `${ITEM_WIDTH}px`,
marginLeft: `${-ITEM_WIDTH / 2}px`,
top: 0,
height: '100%',
}}
>
{/* Dot — centered exactly on the line (70px - 4.5px) */}
<div
className='-translate-x-1/2 absolute top-[66px] left-1/2 h-[9px] w-[9px] rounded-full transition-[filter,transform] duration-150 group-hover:scale-150 group-hover:brightness-150'
style={{ backgroundColor: color, opacity: 0.85 }}
/>
{/* Stem + label above */}
{isAbove && (
<div className='-translate-x-1/2 absolute bottom-[74px] left-1/2 flex flex-col items-center'>
<div className='flex flex-col items-center gap-0.5 pb-1.5'>
<span className='whitespace-nowrap font-medium text-[12px] text-[var(--landing-text)] leading-none tracking-[-0.01em] transition-colors group-hover:text-white'>
{model.displayName}
</span>
<span className='whitespace-nowrap font-mono text-[10px] text-[var(--landing-text-muted)] leading-none'>
{formatShortDate(dateStr)}
</span>
</div>
<div
className='w-px'
style={{ height: '10px', backgroundColor: color, opacity: 0.2 }}
/>
</div>
)}
{/* Stem + label below */}
{!isAbove && (
<div className='-translate-x-1/2 absolute top-[75px] left-1/2 flex flex-col items-center'>
<div
className='w-px'
style={{ height: '10px', backgroundColor: color, opacity: 0.2 }}
/>
<div className='flex flex-col items-center gap-0.5 pt-1.5'>
<span className='whitespace-nowrap font-medium text-[12px] text-[var(--landing-text)] leading-none tracking-[-0.01em] transition-colors group-hover:text-white'>
{model.displayName}
</span>
<span className='whitespace-nowrap font-mono text-[10px] text-[var(--landing-text-muted)] leading-none'>
{formatShortDate(dateStr)}
</span>
</div>
</div>
)}
</Link>
)
})}
</div>
</div>
</section>
)
}
+99 -135
View File
@@ -1,10 +1,15 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import { ModelComparisonCharts } from '@/app/(landing)/models/components/model-comparison-charts'
import { ModelDirectory } from '@/app/(landing)/models/components/model-directory'
import { ModelCard, ProviderCard } from '@/app/(landing)/models/components/model-primitives'
import {
FeaturedModelCard,
FeaturedProviderCard,
} from '@/app/(landing)/models/components/model-primitives'
import {
ALL_CATALOG_MODELS,
getPricingBounds,
MODEL_CATALOG_PROVIDERS,
MODEL_PROVIDERS_WITH_CATALOGS,
@@ -17,24 +22,29 @@ const baseUrl = getBaseUrl()
const faqItems = [
{
question: 'What is the Sim AI models directory?',
question: 'Which AI models are best for building agents and automated workflows?',
answer:
'The Sim AI models directory is a public catalog of the language models and providers tracked inside Sim. It shows provider coverage, model IDs, pricing per one million tokens, context windows, and supported capabilities such as reasoning controls, structured outputs, and deep research.',
'The most important factors for agent tasks are reliable tool use (function calling), a large enough context window to track conversation history and tool outputs, and consistent instruction following. In Sim, OpenAI GPT-4.1, Anthropic Claude Sonnet, and Google Gemini 2.5 Pro are popular choices — each supports tool use, structured outputs, and context windows of 128K tokens or more. For cost-sensitive or high-throughput agents, Groq and Cerebras offer significantly faster inference at lower cost.',
},
{
question: 'Can I compare models from multiple providers in one place?',
question: 'What does context window size mean when running an AI agent?',
answer:
'Yes. This page organizes every tracked model by provider and lets you search across providers, model names, and capabilities. You can quickly compare OpenAI, Anthropic, Google, xAI, Mistral, Groq, Cerebras, Fireworks, Bedrock, and more from a single directory.',
'The context window is the total number of tokens a model can process in a single call, including your system prompt, conversation history, tool call results, and any documents you pass in. For agents running multi-step tasks, context fills up quickly — each tool result and each retrieved document adds tokens. A 128K-token context window fits roughly 300 pages of text; models like Gemini 2.5 Pro support up to 1M tokens, enough to hold an entire codebase in a single pass.',
},
{
question: 'Are these model prices shown per million tokens?',
question: 'Are model prices shown per million tokens?',
answer:
'Yes. Input, cached input, and output prices on this page are shown per one million tokens based on the provider metadata tracked in Sim.',
'Yes. Input, cached input, and output prices are all listed per one million tokens, matching how providers bill through their APIs. For agents that chain multiple calls, costs compound quickly — an agent completing 100 turns at 10K tokens each consumes roughly 1M tokens per session. Cached input pricing applies when a provider supports prompt caching, where a repeated prefix like a system prompt is billed at a reduced rate.',
},
{
question: 'Does Sim support providers with dynamic model catalogs too?',
question: 'Which AI models support tool use and function calling?',
answer:
'Yes. Some providers such as OpenRouter, Fireworks, Ollama, and vLLM load their model lists dynamically at runtime. Those providers are still shown here even when their full public model list is not hard-coded into the catalog.',
'Tool use — also called function calling — lets an agent invoke external APIs, query databases, run code, or take any action you define. In Sim, all first-party models from OpenAI, Anthropic, Google, Mistral, Groq, Cerebras, and xAI support tool use. Look for the Tool Use capability tag on any model card in this directory to confirm support.',
},
{
question: 'How do I add a model to a Sim agent workflow?',
answer:
'Open any workflow in Sim, add an Agent block, and select your provider and model from the model picker inside that block. Every model listed in this directory is available in the Agent block. Swapping models takes one click and does not affect the rest of your workflow, making it straightforward to test different models on the same task without rebuilding anything.',
},
]
@@ -82,15 +92,15 @@ export default function ModelsPage() {
const flatModels = MODEL_CATALOG_PROVIDERS.flatMap((provider) =>
provider.models.map((model) => ({ provider, model }))
)
const featuredProviders = MODEL_PROVIDERS_WITH_CATALOGS.slice(0, 6)
const featuredModels = MODEL_PROVIDERS_WITH_CATALOGS.flatMap((provider) =>
provider.featuredModels[0] ? [{ provider, model: provider.featuredModels[0] }] : []
).slice(0, 6)
const heroProviders = ['openai', 'anthropic', 'azure-openai', 'google', 'bedrock']
.map((providerId) => MODEL_CATALOG_PROVIDERS.find((provider) => provider.id === providerId))
.filter(
(provider): provider is (typeof MODEL_CATALOG_PROVIDERS)[number] => provider !== undefined
const featuredProviderOrder = ['anthropic', 'openai', 'google']
const featuredProviders = featuredProviderOrder
.map((id) => MODEL_PROVIDERS_WITH_CATALOGS.find((p) => p.id === id))
.filter((p): p is (typeof MODEL_PROVIDERS_WITH_CATALOGS)[number] => p !== undefined)
const featuredModels = featuredProviders
.map((provider) =>
provider.featuredModels[0] ? { provider, model: provider.featuredModels[0] } : null
)
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
@@ -159,135 +169,89 @@ export default function ModelsPage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className='mx-auto max-w-[1280px] px-6 py-16 sm:px-8 md:px-12'>
<section aria-labelledby='models-heading' className='mb-14'>
<div className='max-w-[840px]'>
<p className='mb-3 text-[12px] text-[var(--landing-text-muted)] uppercase tracking-[0.16em]'>
Public model directory
</p>
<section className='bg-[var(--landing-bg)]'>
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
<Badge
variant='blue'
size='md'
dot
className='mb-5 bg-white/10 font-season text-white uppercase tracking-[0.02em]'
>
Models
</Badge>
<div className='flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between'>
<h1
id='models-heading'
className='text-balance font-[500] text-[40px] text-[var(--landing-text)] leading-tight sm:text-[56px]'
className='text-balance text-[28px] text-white leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
Browse AI models by provider, pricing, and capabilities
Models
</h1>
<p className='mt-5 max-w-[760px] text-[18px] text-[var(--landing-text-muted)] leading-relaxed'>
Explore every model tracked in Sim across providers like{' '}
{heroProviders.map((provider, index, allProviders) => {
const Icon = provider.icon
return (
<span key={provider.id}>
<span className='inline-flex items-center gap-1 whitespace-nowrap align-[0.02em]'>
{Icon ? (
<span
aria-hidden='true'
className='relative top-[0.02em] inline-flex shrink-0 text-[var(--landing-text)]'
>
<Icon className='h-[0.82em] w-[0.82em]' />
</span>
) : null}
<span>{provider.name}</span>
</span>
{index < allProviders.length - 1 ? ', ' : ''}
</span>
)
})}
{
' and more. Compare model IDs, token pricing, context windows, and features such as reasoning, structured outputs, and deep research from one clean catalog.'
}
<p className='font-[430] font-season text-[var(--landing-text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
Browse {TOTAL_MODELS} AI models across {TOTAL_MODEL_PROVIDERS} providers. Compare
pricing, context windows, and capabilities.
</p>
</div>
</div>
<div className='mt-8 flex flex-wrap gap-3'>
<a
href='https://sim.ai'
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--white)] bg-[var(--white)] px-3 font-[430] text-[14px] text-[var(--landing-text-dark)] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Start building free
</a>
<Link
href='/integrations'
className='inline-flex h-[34px] items-center rounded-[6px] border border-[var(--landing-border-strong)] px-3 font-[430] text-[14px] text-[var(--landing-text)] transition-colors hover:bg-[var(--landing-bg-elevated)]'
>
Explore integrations
</Link>
</div>
</section>
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
<section aria-labelledby='providers-heading' className='mb-16'>
<div className='mb-6'>
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
{featuredProviders.length > 0 && (
<>
<nav aria-label='Featured providers' className='flex flex-col sm:flex-row'>
{featuredProviders.map((provider) => (
<FeaturedProviderCard key={provider.id} provider={provider} />
))}
</nav>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
{featuredModels.length > 0 && (
<>
<nav aria-label='Featured models' className='flex flex-col sm:flex-row'>
{featuredModels.map(({ provider, model }) => (
<FeaturedModelCard key={model.id} provider={provider} model={model} />
))}
</nav>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
</>
)}
<ModelComparisonCharts models={ALL_CATALOG_MODELS} />
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<section aria-labelledby='all-models-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='all-models-heading'
className='mb-2 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
All models
</h2>
</div>
<ModelDirectory />
</section>
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
<section aria-labelledby='faq-heading' className='px-6 py-10'>
<h2
id='providers-heading'
className='font-[500] text-[28px] text-[var(--landing-text)]'
id='faq-heading'
className='mb-8 text-[20px] text-white leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
Browse by provider
Frequently asked questions
</h2>
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
Each provider has its own generated SEO page with model lineup details, featured
models, provider FAQs, and internal links to individual model pages.
</p>
</div>
<div>
<LandingFAQ faqs={faqItems} />
</div>
</section>
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3'>
{featuredProviders.map((provider) => (
<ProviderCard key={provider.id} provider={provider} />
))}
</div>
</section>
<section aria-labelledby='featured-models-heading' className='mb-16'>
<div className='mb-6'>
<h2
id='featured-models-heading'
className='font-[500] text-[28px] text-[var(--landing-text)]'
>
Featured model pages
</h2>
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
These pages are generated directly from the model registry and target high-intent
search queries around pricing, context windows, and model capabilities.
</p>
</div>
<div className='grid grid-cols-1 gap-4 xl:grid-cols-2'>
{featuredModels.map(({ provider, model }) => (
<ModelCard key={model.id} provider={provider} model={model} showProvider />
))}
</div>
</section>
<section aria-labelledby='all-models-heading'>
<div className='mb-6'>
<h2
id='all-models-heading'
className='font-[500] text-[28px] text-[var(--landing-text)]'
>
All models
</h2>
<p className='mt-2 max-w-[760px] text-[15px] text-[var(--landing-text-muted)] leading-relaxed'>
Search the full catalog by provider, model ID, or capability. Use it to compare
providers, sanity-check pricing, and quickly understand which models fit the workflow
you&apos;re building. All pricing is shown per one million tokens using the metadata
currently tracked in Sim.
</p>
</div>
<ModelDirectory />
</section>
<section
aria-labelledby='faq-heading'
className='mt-16 rounded-3xl border border-[var(--landing-border)] bg-[var(--landing-bg-card)] p-6 sm:p-8'
>
<h2 id='faq-heading' className='font-[500] text-[28px] text-[var(--landing-text)]'>
Frequently asked questions
</h2>
<div className='mt-3'>
<LandingFAQ faqs={faqItems} />
</div>
</section>
</div>
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
</section>
</>
)
}
+44 -17
View File
@@ -13,12 +13,6 @@ const PROVIDER_PREFIXES: Record<string, string[]> = {
vllm: ['vllm/'],
}
const PROVIDER_NAME_OVERRIDES: Record<string, string> = {
deepseek: 'DeepSeek',
vllm: 'vLLM',
xai: 'xAI',
}
const TOKEN_REPLACEMENTS: Record<string, string> = {
ai: 'AI',
aws: 'AWS',
@@ -108,6 +102,7 @@ export interface CatalogModel {
providerName: string
providerSlug: string
contextWindow: number | null
releaseDate: string | null
pricing: PricingInfo
capabilities: ModelCapabilities
capabilityTags: string[]
@@ -126,6 +121,8 @@ export interface CatalogProvider {
defaultModel: string
defaultModelDisplayName: string
icon?: ComponentType<{ className?: string }>
color?: string
isReseller: boolean
contextInformationAvailable: boolean
providerCapabilityTags: string[]
modelCount: number
@@ -418,10 +415,6 @@ function buildModelSummary(
return parts.filter(Boolean).join(' ')
}
function getProviderDisplayName(providerId: string, providerName: string): string {
return PROVIDER_NAME_OVERRIDES[providerId] ?? providerName
}
function computeModelRelevanceScore(model: CatalogModel): number {
return (
(model.capabilities.reasoningEffort ? 10 : 0) +
@@ -438,7 +431,7 @@ function compareModelsByRelevance(a: CatalogModel, b: CatalogModel): number {
const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
const providerSlug = slugify(provider.id)
const providerDisplayName = getProviderDisplayName(provider.id, provider.name)
const providerDisplayName = provider.name
const providerCapabilityTags = buildCapabilityTags(provider.capabilities ?? {})
const models: CatalogModel[] = provider.models.map((model) => {
@@ -464,6 +457,7 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
providerName: providerDisplayName,
providerSlug,
contextWindow: model.contextWindow ?? null,
releaseDate: model.releaseDate ?? null,
pricing: model.pricing,
capabilities: mergedCapabilities,
capabilityTags,
@@ -507,6 +501,8 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
defaultModel: provider.defaultModel,
defaultModelDisplayName,
icon: provider.icon,
color: provider.color,
isReseller: provider.isReseller ?? false,
contextInformationAvailable: provider.contextInformationAvailable !== false,
providerCapabilityTags,
modelCount: models.length,
@@ -514,7 +510,6 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
featuredModels,
searchText: [
provider.name,
providerDisplayName,
provider.id,
provider.description,
provider.defaultModel,
@@ -631,7 +626,13 @@ export function buildProviderFaqs(provider: CatalogProvider): CatalogFaq[] {
const cheapestModel = getCheapestProviderModel(provider)
const largestContextModel = getLargestContextProviderModel(provider)
return [
const toolUseModels = provider.models.filter(
(m) =>
m.capabilities.toolUsageControl !== undefined ||
provider.providerCapabilityTags.includes('Tool Use')
)
const faqs: CatalogFaq[] = [
{
question: `What ${provider.name} models are available in Sim?`,
answer: `Sim currently tracks ${provider.modelCount} ${provider.name} model${provider.modelCount === 1 ? '' : 's'} including ${provider.models
@@ -662,10 +663,27 @@ export function buildProviderFaqs(provider: CatalogProvider): CatalogFaq[] {
: `Context window details are not fully available for every ${provider.name} model in the public catalog.`,
},
]
if (toolUseModels.length > 0) {
faqs.push({
question: `Which ${provider.name} models support tool use and function calling in Sim?`,
answer:
toolUseModels.length === provider.modelCount
? `All ${provider.name} models in Sim support tool use and function calling, allowing agents to invoke external APIs, query databases, and run custom actions.`
: `${toolUseModels
.slice(0, 5)
.map((m) => m.displayName)
.join(
', '
)}${toolUseModels.length > 5 ? ', and others' : ''} support tool use and function calling in Sim, enabling agents to invoke external APIs and run custom actions.`,
})
}
return faqs
}
export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel): CatalogFaq[] {
return [
const faqs: CatalogFaq[] = [
{
question: `What is ${model.displayName}?`,
answer: `${model.displayName} is a ${provider.name} model available in Sim. ${model.summary}`,
@@ -677,17 +695,26 @@ export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel):
{
question: `What is the context window for ${model.displayName}?`,
answer: model.contextWindow
? `${model.displayName} supports a listed context window of ${formatTokenCount(model.contextWindow)} tokens in Sim.`
? `${model.displayName} supports a context window of ${formatTokenCount(model.contextWindow)} tokens in Sim. In an agent workflow, this determines how much conversation history, tool outputs, and retrieved documents the model can hold in a single call.`
: `A public context window value is not currently tracked for ${model.displayName}.`,
},
{
question: `What capabilities does ${model.displayName} support?`,
answer:
model.capabilityTags.length > 0
? `${model.displayName} supports ${model.capabilityTags.join(', ')}.`
: `${model.displayName} is available in Sim, but no extra public capability flags are currently tracked for this model.`,
? `${model.displayName} supports the following capabilities in Sim: ${model.capabilityTags.join(', ')}.`
: `${model.displayName} supports standard text generation in Sim. No additional capability flags such as tool use or structured outputs are currently tracked for this model.`,
},
]
if (model.bestFor) {
faqs.push({
question: `What is ${model.displayName} best used for?`,
answer: `${model.bestFor} When used in a Sim workflow, it can be selected in any Agent block from the model picker.`,
})
}
return faqs
}
export function buildModelCapabilityFacts(model: CatalogModel): CapabilityFact[] {
+1 -1
View File
@@ -11,7 +11,7 @@ export const ApifyBlock: BlockConfig<RunActorResult> = {
'Integrate Apify into your workflow. Run any Apify actor with custom input and retrieve results. Supports both synchronous and asynchronous execution with automatic dataset fetching.',
docsLink: 'https://docs.sim.ai/tools/apify',
category: 'tools',
integrationType: IntegrationType.Automation,
integrationType: IntegrationType.Search,
tags: ['web-scraping', 'automation', 'data-analytics'],
bgColor: '#E0E0E0',
icon: ApifyIcon,
+1 -1
View File
@@ -12,7 +12,7 @@ export const ApolloBlock: BlockConfig<ApolloResponse> = {
'Integrates Apollo.io into the workflow. Search for people and companies, enrich contact data, manage your CRM contacts and accounts, add contacts to sequences, and create tasks.',
docsLink: 'https://docs.sim.ai/tools/apollo',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['enrichment', 'sales-engagement'],
bgColor: '#EBF212',
icon: ApolloIcon,
+1 -1
View File
@@ -11,7 +11,7 @@ export const BrandfetchBlock: BlockConfig<BrandfetchGetBrandResponse | Brandfetc
'Integrate Brandfetch into your workflow. Retrieve brand logos, colors, fonts, and company data by domain, ticker, or name search.',
docsLink: 'https://docs.sim.ai/tools/brandfetch',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['enrichment', 'marketing'],
bgColor: '#000000',
icon: BrandfetchIcon,
+1 -1
View File
@@ -11,7 +11,7 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser.',
docsLink: 'https://docs.sim.ai/tools/browser_use',
category: 'tools',
integrationType: IntegrationType.Automation,
integrationType: IntegrationType.AI,
tags: ['web-scraping', 'automation', 'agentic'],
bgColor: '#181C1E',
icon: BrowserUseIcon,
+1 -1
View File
@@ -10,7 +10,7 @@ export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
longDescription: 'Integrate Clay into the workflow. Can populate a table with data.',
docsLink: 'https://docs.sim.ai/tools/clay',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['enrichment', 'sales-engagement', 'data-analytics'],
bgColor: '#E0E0E0',
icon: ClayIcon,
+1 -1
View File
@@ -11,7 +11,7 @@ export const DagsterBlock: BlockConfig<DagsterResponse> = {
'Connect to a Dagster instance to launch job runs, monitor run status, list available jobs across repositories, terminate or delete runs, reexecute failed runs, fetch run logs, and manage schedules and sensors. API token only required for Dagster+.',
docsLink: 'https://docs.sim.ai/tools/dagster',
category: 'tools',
integrationType: IntegrationType.Automation,
integrationType: IntegrationType.Analytics,
tags: ['data-analytics', 'automation'],
bgColor: '#ffffff',
icon: DagsterIcon,
+1 -1
View File
@@ -10,7 +10,7 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.',
docsLink: 'https://docs.sim.ai/tools/elevenlabs',
category: 'tools',
integrationType: IntegrationType.Media,
integrationType: IntegrationType.AI,
tags: ['text-to-speech'],
bgColor: '#181C1E',
icon: ElevenLabsIcon,
+1 -1
View File
@@ -11,7 +11,7 @@ export const EnrichBlock: BlockConfig = {
'Access real-time B2B data intelligence with Enrich.so. Enrich profiles from email addresses, find work emails from LinkedIn, verify email deliverability, search for people and companies, and analyze LinkedIn post engagement.',
docsLink: 'https://docs.enrich.so/',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['enrichment', 'data-analytics'],
bgColor: '#E5E5E6',
icon: EnrichSoIcon,
+1 -1
View File
@@ -12,7 +12,7 @@ export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
'Integrate Firecrawl into the workflow. Scrape pages, search the web, crawl entire sites, map URL structures, and extract structured data with AI.',
docsLink: 'https://docs.sim.ai/tools/firecrawl',
category: 'tools',
integrationType: IntegrationType.Automation,
integrationType: IntegrationType.Search,
tags: ['web-scraping', 'automation'],
bgColor: '#181C1E',
icon: FirecrawlIcon,
+2 -2
View File
@@ -17,7 +17,7 @@ export const FirefliesBlock: BlockConfig<FirefliesResponse> = {
'Integrate Fireflies.ai into the workflow. Manage meeting transcripts, add bot to live meetings, create soundbites, and more. Can also trigger workflows when transcriptions complete.',
docsLink: 'https://docs.sim.ai/tools/fireflies',
category: 'tools',
integrationType: IntegrationType.Media,
integrationType: IntegrationType.Productivity,
tags: ['meeting', 'speech-to-text', 'note-taking'],
icon: FirefliesIcon,
bgColor: '#100730',
@@ -615,7 +615,7 @@ export const FirefliesV2Block: BlockConfig<FirefliesResponse> = {
name: 'Fireflies',
description: 'Interact with Fireflies.ai meeting transcripts and recordings',
hideFromToolbar: false,
integrationType: IntegrationType.Media,
integrationType: IntegrationType.Productivity,
tags: ['meeting', 'speech-to-text', 'note-taking'],
subBlocks: firefliesV2SubBlocks,
tools: {
+1 -1
View File
@@ -12,7 +12,7 @@ export const GongBlock: BlockConfig<GongResponse> = {
'Integrate Gong into your workflow. Access call recordings, transcripts, user data, activity stats, scorecards, trackers, library content, coaching metrics, and more via the Gong API.',
docsLink: 'https://docs.sim.ai/tools/gong',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['meeting', 'sales-engagement', 'speech-to-text'],
bgColor: '#8039DF',
icon: GongIcon,
+1 -1
View File
@@ -13,7 +13,7 @@ export const GrainBlock: BlockConfig = {
longDescription:
'Integrate Grain into your workflow. Access meeting recordings, transcripts, highlights, and AI-generated summaries. Can also trigger workflows based on Grain webhook events.',
category: 'tools',
integrationType: IntegrationType.Media,
integrationType: IntegrationType.Productivity,
tags: ['meeting', 'note-taking'],
docsLink: 'https://docs.sim.ai/tools/grain',
icon: GrainIcon,
+1 -1
View File
@@ -11,7 +11,7 @@ export const HunterBlock: BlockConfig<HunterResponse> = {
'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses.',
docsLink: 'https://docs.sim.ai/tools/hunter',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['enrichment', 'sales-engagement'],
bgColor: '#E0E0E0',
icon: HunterIOIcon,
+1 -1
View File
@@ -13,7 +13,7 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
'Integrate LinkedIn into workflows. Share posts to your personal feed and access your LinkedIn profile information.',
docsLink: 'https://docs.sim.ai/tools/linkedin',
category: 'tools',
integrationType: IntegrationType.Social,
integrationType: IntegrationType.Sales,
tags: ['marketing', 'sales-engagement', 'enrichment'],
bgColor: '#0072B1',
icon: LinkedInIcon,
+1 -1
View File
@@ -13,7 +13,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
'Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, manage messages, and access user and subreddit info.',
docsLink: 'https://docs.sim.ai/tools/reddit',
category: 'tools',
integrationType: IntegrationType.Social,
integrationType: IntegrationType.Communication,
tags: ['content-management', 'web-scraping'],
bgColor: '#FF5700',
icon: RedditIcon,
+1 -1
View File
@@ -9,7 +9,7 @@ export const SixtyfourBlock: BlockConfig = {
'Find emails, phone numbers, and enrich lead or company data with contact information, social profiles, and detailed research using Sixtyfour AI.',
docsLink: 'https://docs.sim.ai/tools/sixtyfour',
category: 'tools',
integrationType: IntegrationType.SalesIntelligence,
integrationType: IntegrationType.Sales,
tags: ['enrichment', 'sales-engagement'],
bgColor: '#000000',
icon: SixtyfourIcon,
+1 -1
View File
@@ -13,7 +13,7 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
'Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks.',
docsLink: 'https://docs.sim.ai/tools/spotify',
category: 'tools',
integrationType: IntegrationType.Media,
integrationType: IntegrationType.Communication,
tags: ['content-management', 'automation'],
hideFromToolbar: true,
bgColor: '#000000',
+1 -1
View File
@@ -35,7 +35,7 @@ export const StagehandBlock: BlockConfig<StagehandResponse> = {
'Integrate Stagehand into the workflow. Can extract structured data from webpages or run an autonomous agent to perform tasks.',
docsLink: 'https://docs.sim.ai/tools/stagehand',
category: 'tools',
integrationType: IntegrationType.Automation,
integrationType: IntegrationType.AI,
tags: ['web-scraping', 'automation', 'agentic'],
bgColor: '#FFC83C',
icon: StagehandIcon,
+1 -1
View File
@@ -12,7 +12,7 @@ export const XBlock: BlockConfig = {
'Integrate X into the workflow. Search tweets, manage bookmarks, follow/block/mute users, like and retweet, view trends, and more.',
docsLink: 'https://docs.sim.ai/tools/x',
category: 'tools',
integrationType: IntegrationType.Social,
integrationType: IntegrationType.Communication,
tags: ['marketing', 'messaging'],
bgColor: '#000000',
icon: xIcon,
+1 -1
View File
@@ -12,7 +12,7 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
'Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments.',
docsLink: 'https://docs.sim.ai/tools/youtube',
category: 'tools',
integrationType: IntegrationType.Media,
integrationType: IntegrationType.Communication,
tags: ['google-workspace', 'marketing', 'content-management'],
bgColor: '#FF0000',
icon: YouTubeIcon,
+1 -4
View File
@@ -19,7 +19,6 @@ export type BlockCategory = 'blocks' | 'tools' | 'triggers'
export enum IntegrationType {
AI = 'ai',
Analytics = 'analytics',
Automation = 'automation',
Communication = 'communication',
CRM = 'crm',
CustomerSupport = 'customer-support',
@@ -31,13 +30,11 @@ export enum IntegrationType {
Email = 'email',
FileStorage = 'file-storage',
HR = 'hr',
Media = 'media',
Other = 'other',
Productivity = 'productivity',
SalesIntelligence = 'sales-intelligence',
Sales = 'sales',
Search = 'search',
Security = 'security',
Social = 'social',
}
export type IntegrationTag =
File diff suppressed because it is too large Load Diff
+89 -6
View File
@@ -100,7 +100,7 @@ interface IntegrationEntry {
triggerCount: number
authType: 'oauth' | 'api-key' | 'none'
category: string
integrationType?: string
integrationTypes?: string[]
tags?: string[]
}
@@ -676,7 +676,14 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
triggerCount: triggers.length,
authType,
category: config.category,
...(config.integrationType ? { integrationType: config.integrationType } : {}),
...(config.integrationType || config.tags
? {
integrationTypes: deriveIntegrationTypes(
config.integrationType || null,
config.tags || []
),
}
: {}),
...(config.tags ? { tags: config.tags } : {}),
})
}
@@ -930,6 +937,85 @@ function extractStringPropertyFromContent(
return null
}
/**
* Tag-to-category mapping used by deriveIntegrationTypes to expand a block's
* primary integrationType into a full set of categories for the landing page.
*/
const TAG_TO_CATEGORIES: Record<string, string[]> = {
llm: ['ai'],
agentic: ['ai'],
'image-generation': ['ai', 'design'],
'video-generation': ['ai', 'design'],
'text-to-speech': ['ai'],
'speech-to-text': ['ai'],
ocr: ['ai', 'documents'],
'vector-search': ['ai', 'search'],
'document-processing': ['documents'],
'content-management': ['documents'],
'e-signatures': ['documents'],
'note-taking': ['productivity', 'documents'],
'knowledge-base': ['documents', 'search'],
'data-analytics': ['analytics'],
seo: ['analytics', 'search'],
monitoring: ['developer-tools', 'analytics'],
'error-tracking': ['developer-tools'],
'incident-management': ['developer-tools'],
'version-control': ['developer-tools'],
'ci-cd': ['developer-tools'],
'feature-flags': ['developer-tools'],
messaging: ['communication'],
meeting: ['communication', 'productivity'],
calendar: ['productivity'],
scheduling: ['productivity'],
'project-management': ['productivity'],
ticketing: ['productivity', 'customer-support'],
forms: ['productivity'],
spreadsheet: ['productivity', 'databases'],
'data-warehouse': ['databases'],
cloud: ['developer-tools'],
'web-scraping': ['search'],
'sales-engagement': ['sales'],
enrichment: ['sales'],
'email-marketing': ['email'],
marketing: ['analytics'],
payments: ['ecommerce'],
subscriptions: ['ecommerce'],
hiring: ['hr'],
identity: ['security'],
'secrets-management': ['security'],
'customer-support': ['customer-support'],
webhooks: ['developer-tools'],
automation: ['developer-tools'],
}
/**
* Derive the full list of integration type categories from a block's primary
* integrationType and its tags. The primary type is always first; additional
* categories are inferred from tags via TAG_TO_CATEGORIES.
*/
function deriveIntegrationTypes(primaryType: string | null, tags: string[]): string[] {
const types = new Set<string>()
if (primaryType) {
types.add(primaryType)
}
for (const tag of tags) {
const mapped = TAG_TO_CATEGORIES[tag]
if (mapped) {
for (const t of mapped) {
types.add(t)
}
}
}
// Return primary first, then the rest sorted for deterministic output
const result: string[] = []
if (primaryType && types.has(primaryType)) {
result.push(primaryType)
types.delete(primaryType)
}
result.push(...Array.from(types).sort())
return result
}
/**
* Extract an enum property value from block content.
* Matches patterns like `integrationType: IntegrationType.DeveloperTools`
@@ -943,7 +1029,6 @@ function extractEnumPropertyFromContent(content: string, propName: string): stri
const ENUM_MAP: Record<string, string> = {
AI: 'ai',
Analytics: 'analytics',
Automation: 'automation',
Communication: 'communication',
CRM: 'crm',
CustomerSupport: 'customer-support',
@@ -955,13 +1040,11 @@ function extractEnumPropertyFromContent(content: string, propName: string): stri
Email: 'email',
FileStorage: 'file-storage',
HR: 'hr',
Media: 'media',
Other: 'other',
Productivity: 'productivity',
SalesIntelligence: 'sales-intelligence',
Sales: 'sales',
Search: 'search',
Security: 'security',
Social: 'social',
}
return ENUM_MAP[enumKey] || enumKey.toLowerCase()
}