feat(landing): template, generic workflow

This commit is contained in:
Emir Karabeg
2026-02-20 14:04:58 -08:00
parent e47dcdcc43
commit bb3e899f74
12 changed files with 1550 additions and 151 deletions

View File

@@ -0,0 +1,26 @@
---
description: SEO and GEO guidelines for the landing page
globs: ["apps/sim/app/(home)/**/*.tsx"]
---
# Landing Page — SEO / GEO
## SEO
- One `<h1>` per page, in Hero only — never add another.
- Strict heading hierarchy: H1 (Hero) → H2 (section titles) → H3 (feature names).
- Every section: `<section id="…" aria-labelledby="…-heading">`.
- Decorative/animated elements: `aria-hidden="true"`.
- All internal routes use Next.js `<Link>` (crawlable). External links get `rel="noopener noreferrer"`.
- Navbar is a Server Component (no `'use client'`) for immediate crawlability. Logo `<Image>` has `priority` (LCP element).
- Navbar `<nav>` carries `SiteNavigationElement` schema.org markup.
- Feature lists must stay in sync with `WebApplication.featureList` in `structured-data.tsx`.
## GEO (Generative Engine Optimisation)
- **Answer-first pattern**: each section's H2 + subtitle should directly answer a user question (e.g. "What is Sim?", "How fast can I deploy?").
- **Atomic answer blocks**: each feature / template card should be independently extractable by an AI summariser.
- **Entity consistency**: always write "Sim" by name — never "the platform" or "our tool".
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI agents", "agentic workflows".
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.

View File

@@ -1,17 +1,385 @@
/**
* Features section — Sim's core product capabilities.
*
* SEO:
* - `<section id="features" aria-labelledby="features-heading">`.
* - `<h2 id="features-heading">` for the section title.
* - Each feature: `<h3>` + `<p>` + `<ul>` capability list. Strict H2 -> H3 -> H4 hierarchy.
* - Feature lists map to `WebApplication.featureList` in structured-data.tsx — keep in sync.
*
* GEO:
* - Each feature block is independently extractable (atomic answer block pattern).
* - Include specific numbers ("1,000+ integrations", "15+ AI providers", "SOC2 compliant").
* - Always use "Sim" by name — never "the platform" or "our tool" (entity consistency).
*/
export default function Features() {
return null
import { ArrowUp, AtSign, Paperclip } from 'lucide-react'
const HEADING = 'font-[550] font-season text-xl tracking-tight text-[#212121]'
const DESC =
'mt-4 max-w-[300px] font-[550] font-season text-sm leading-[125%] tracking-wide text-[#1C1C1C]/60'
const MODEL_ROWS = [
{
stagger: '19%',
models: [
{ name: 'Gemini 1.5 Pro', provider: 'Google' },
{ name: 'LLaMA 3', provider: 'Meta' },
{ name: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
],
},
{
stagger: '29%',
models: [
{ name: 'GPT-4.1 / GPT-4o', provider: 'OpenAI' },
{ name: 'Mistral Large', provider: 'Mistral AI' },
{ name: 'Grok-2', provider: 'xAI' },
],
},
{
stagger: '9%',
models: [
{ name: 'Gemini 1.5 Pro', provider: 'Google' },
{ name: 'LLaMA 3', provider: 'Meta' },
{ name: 'Claude 3.5 Sonnet', provider: 'Anthropic' },
],
},
] as const
const COPILOT_ACTIONS = [
{ color: '#00F701', title: 'Build & edit workflows', subtitle: 'Help me build a workflow' },
{ color: '#F891E8', title: 'Debug workflows', subtitle: 'Help me debug my workflow' },
{ color: '#F04E9B', title: 'Optimize workflows', subtitle: 'help me optimize my workflow' },
] as const
const WORKFLOW_LOGS = [
{
name: 'main-relativity',
color: '#3367F5',
dots: [1, 2, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
},
{
name: 'readDocuments',
color: '#7632F5',
dots: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
},
{
name: 'Escalation Triage...',
color: '#F218B9',
dots: [1, 2, 2, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
},
{
name: 'acceptance/rejection',
color: '#F34E25',
dots: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
] as const
const DOT_COLORS = ['#E0E0E0', '#00F701', '#FF6464'] as const
function ModelPill({ name, provider }: { name: string; provider: string }) {
return (
<div className='flex shrink-0 items-center gap-1 whitespace-nowrap rounded-full border border-[#D6D6D6] bg-white px-3 py-2 shadow-[0_1px_5px_rgba(0,0,0,0.05)]'>
<svg className='h-4 w-4 shrink-0 text-black/60' viewBox='0 0 16 16' fill='none'>
<path
d='M2 3.5h12M2 8h12M2 12.5h12'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
/>
</svg>
<span className='font-[350] text-black/80 text-sm'>{name}</span>
<span className='font-[350] text-black/40 text-sm'>{provider}</span>
</div>
)
}
function CopilotCell() {
return (
<div className='relative flex flex-[27] flex-col overflow-hidden'>
<div className='pointer-events-none absolute inset-x-0 bottom-0 h-1/2 rounded-t-lg border border-[#E9E9E9] bg-[#F3F3F3]' />
<div className='relative z-10 p-5'>
<h3 className={HEADING}>Co-pilot</h3>
<p className={DESC}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup, so
suggestions are relevant and actionable. From quick edits to deeper reasoning, Copilot
works alongside you at every step.
</p>
<p className='mt-5 font-[550] font-season text-[#1C1C1C] text-sm tracking-tight'>
Try it out
</p>
</div>
<div className='relative z-10 mt-auto px-4 pb-5' aria-hidden='true'>
<div className='absolute inset-x-2 top-[-18px] bottom-0 rounded-lg border border-[#EAEAEA] bg-white/80' />
<div className='relative flex flex-col gap-2.5'>
<div className='w-[150%] rounded-md border border-black/10 bg-white shadow-[0_1px_2px_#EBEBEB]'>
<div className='flex flex-col gap-3 p-2'>
<div className='flex h-6 w-6 items-center justify-center rounded bg-[#FFCC02]'>
<AtSign className='h-3.5 w-3.5 text-[#070707]' strokeWidth={2} />
</div>
<span className='font-[550] font-season text-base text-black/50 tracking-wide'>
Plan, search build anything
</span>
</div>
<div className='flex items-center justify-between p-2'>
<div className='flex items-center gap-1'>
<div className='flex h-6 items-center rounded bg-black/[0.04] px-1.5'>
<span className='font-[550] font-season text-black/60 text-sm'>Build</span>
</div>
<div className='flex h-6 items-center rounded bg-black/[0.04] px-1.5'>
<span className='font-[550] font-season text-black/60 text-sm'>claude..</span>
</div>
</div>
<div className='flex items-center gap-2.5'>
<Paperclip className='h-4 w-4 text-black/50' strokeWidth={1.5} />
<div className='flex h-6 w-6 items-center justify-center rounded-full bg-black'>
<ArrowUp className='h-3.5 w-3.5 text-white' strokeWidth={2.25} />
</div>
</div>
</div>
</div>
<div className='flex w-[150%] gap-2'>
{COPILOT_ACTIONS.map(({ color, title, subtitle }) => (
<div
key={title}
className='flex flex-1 flex-col gap-2 rounded-md border border-black/10 bg-white p-2 shadow-[0_1px_2px_#EBEBEB]'
>
<div className='h-5 w-5 rounded' style={{ backgroundColor: color }} />
<div className='flex flex-col gap-1'>
<span className='font-[550] font-season text-black/80 text-sm'>{title}</span>
<span className='font-[550] font-season text-black/50 text-xs'>{subtitle}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
function ModelsCell() {
return (
<div className='relative flex-[70] overflow-hidden'>
<div className='relative z-10 p-5'>
<h3 className={HEADING}>Models</h3>
<p className={`${DESC} max-w-[440px]`}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup, so
suggestions are relevant and actionable.
</p>
</div>
<div className='relative z-10 mt-6' aria-hidden='true'>
{MODEL_ROWS.map((row, i) => (
<div key={i}>
<div className='border-[#EEE] border-t' />
<div className='flex items-center gap-8 py-2.5' style={{ paddingLeft: row.stagger }}>
{row.models.map((model, j) => (
<ModelPill key={j} name={model.name} provider={model.provider} />
))}
</div>
<div className='border-[#EEE] border-t' />
</div>
))}
</div>
<div
className='pointer-events-none absolute inset-y-0 left-0 z-20 w-1/4'
style={{ background: 'linear-gradient(90deg, #F6F6F6, transparent)' }}
/>
<div
className='pointer-events-none absolute inset-y-0 right-0 z-20 w-1/4'
style={{ background: 'linear-gradient(270deg, #F6F6F6, transparent)' }}
/>
</div>
)
}
function IntegrationsCell() {
return (
<div className='flex flex-[30] items-start overflow-hidden'>
<div className='shrink-0 p-5'>
<h3 className={HEADING}>Integrations</h3>
<p className={`${DESC} max-w-[220px]`}>
Sim works with the most popular tools &amp; applications with over 100+ integrations ready
to connect to your workflows instantly.
</p>
</div>
<div className='ml-auto flex flex-col gap-3 p-5' aria-hidden='true'>
<div className='mx-auto h-9 w-9 rounded border border-[#D6D6D6] bg-white shadow-[0_0_0_2px_#EAEAEA]' />
{[0, 1].map((row) => (
<div key={row} className='flex gap-5'>
{Array.from({ length: 5 }, (_, i) => (
<div
key={i}
className='h-9 w-9 rounded border border-[#D6D6D6] bg-white shadow-[0_0_0_2px_#EAEAEA]'
/>
))}
</div>
))}
<div className='flex gap-4 pl-6'>
{Array.from({ length: 4 }, (_, i) => (
<div key={i} className='h-12 w-11 rounded-sm border border-[#D5D5D5] border-dashed' />
))}
</div>
</div>
</div>
)
}
function DeployCell() {
return (
<div className='relative flex flex-1 flex-col overflow-hidden'>
<div className='pointer-events-none absolute inset-0 m-3 rounded-lg border border-[#E9E9E9] bg-[#F3F3F3]' />
<div className='relative z-10 p-5'>
<h3 className={HEADING}>Deploy / version</h3>
<p className={DESC}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup
</p>
</div>
<div className='relative z-10 mx-5 mt-auto mb-5' aria-hidden='true'>
<div className='max-w-[274px] rounded border border-[#D7D7D7] bg-white'>
<div className='flex items-center justify-between px-2.5 py-2'>
<div className='flex items-center gap-1.5'>
<div className='h-4 w-4 rounded bg-[#00F701]' />
<span className='font-[650] font-season text-[#1C1C1C] text-[9px]'>VERSION 1.2</span>
</div>
<div className='rounded-sm bg-[#00F701] px-1 py-0.5'>
<span className='font-[550] font-season text-[#1C1C1C] text-[9px]'>
[ INITIATE WF-001 ]
</span>
</div>
</div>
<div className='border-[#D7D7D7] border-t' />
<div className='flex flex-col gap-1 px-2.5 py-2'>
<span className='font-[550] font-season text-[#1C1C1C]/60 text-[9px]'>
Latest deployment: 12:0191:10198.00
</span>
<span className='font-[550] font-season text-[#1C1C1C]/30 text-[9px]'>
Deploy 08: 12:0191:10198.00
</span>
</div>
<div className='border-[#D7D7D7] border-t px-2.5 py-1.5'>
<span className='font-martian-mono font-medium text-[#414141] text-[5px] uppercase'>
TOTAL DEPLOYS: 09
</span>
</div>
</div>
</div>
</div>
)
}
function LogsCell() {
return (
<div className='relative flex flex-1 flex-col overflow-hidden'>
<div className='pointer-events-none absolute inset-0 m-3 rounded-lg border border-[#E9E9E9] bg-[#F3F3F3]' />
<div className='relative z-10 p-5 pb-0'>
<h3 className={HEADING}>Logs</h3>
<p className={DESC}>
Sim Copilot helps design, debug, and optimize workflows. It understands your setup
</p>
</div>
<div className='relative z-10 mx-1.5 mt-auto mb-1.5' aria-hidden='true'>
<div className='rounded border border-[#D7D7D7] bg-white'>
<div className='flex items-center gap-1.5 px-3 py-2.5'>
<div className='flex shrink-0 items-center justify-center rounded bg-[#FCB409] p-1'>
<svg className='h-[9px] w-[9px]' viewBox='0 0 14 14' fill='none'>
<circle cx='7' cy='4.5' r='2' stroke='#1C1C1C' strokeWidth='1.4' />
<path d='M7 7.5V10' stroke='#1C1C1C' strokeWidth='1.4' strokeLinecap='round' />
<circle cx='7' cy='12' r='1.5' stroke='#1C1C1C' strokeWidth='1.4' />
</svg>
</div>
<span className='font-[650] font-season text-[#1C1C1C] text-[10px]'>Logs</span>
</div>
<div className='border-[#D7D7D7]/50 border-t' />
<div className='flex flex-col gap-2 px-3 py-2.5'>
{WORKFLOW_LOGS.map(({ name, color, dots }) => (
<div key={name} className='flex items-center gap-2'>
<div className='h-2 w-2 shrink-0 rounded-sm' style={{ backgroundColor: color }} />
<span className='w-[72px] shrink-0 truncate font-[550] font-season text-[#777] text-[11px] tracking-tight'>
{name}
</span>
<div className='flex flex-1 gap-px'>
{dots.map((d, i) => (
<div
key={i}
className='h-3 flex-1 rounded-full'
style={{ backgroundColor: DOT_COLORS[d] }}
/>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default function Features() {
return (
<section
id='features'
aria-labelledby='features-heading'
className='relative overflow-hidden bg-[#F6F6F6] pb-36'
>
{/* Dark transition from Templates section above */}
<div
aria-hidden='true'
className='absolute top-9 right-0 h-[200px] w-[72%] rounded-b-[9px] border border-[#3E3E3E] bg-[#212121]'
>
<div className='flex h-5 gap-px overflow-hidden'>
<div className='flex-[47] rounded bg-[#2ABBF8] opacity-80' />
<div className='flex-[22] rounded bg-[#00F701] opacity-40' />
<div className='flex-[24] rounded bg-[#00F701] opacity-80' />
<div className='flex-[62] rounded bg-[#2ABBF8] opacity-80' />
</div>
<div className='mt-px flex h-5 gap-px'>
<div className='flex-[27] rounded bg-[#010370]' />
<div className='flex-[24] rounded bg-[#FA4EDF]' />
<div className='flex-[13] rounded bg-[#0607EB]' />
<div className='flex-[27] rounded bg-[#010370]' />
<div className='flex-[24] rounded bg-[#FA4EDF]' />
</div>
</div>
{/* Header */}
<div className='relative z-10 px-20 pt-28'>
<div className='flex flex-col gap-5'>
<div className='inline-flex items-center gap-2 rounded bg-[#1C1C1C]/10 p-1'>
<div className='h-2 w-2 rounded-sm bg-[#1C1C1C]' />
<span className='font-martian-mono font-medium text-[#1C1C1C] text-xs uppercase tracking-[0.02em]'>
how sim works
</span>
</div>
<h2
id='features-heading'
className='font-[550] font-season text-[#1C1C1C] text-[40px] leading-none tracking-tight'
>
Product features
</h2>
</div>
</div>
{/* Feature Grid */}
<div className='relative z-10 mx-20 mt-12'>
<div className='flex overflow-hidden rounded-[9px] border-[#E9E9E9] border-[1.5px]'>
<CopilotCell />
<div className='w-7 shrink-0 border-[#E9E9E9] border-x bg-[#FDFDFD]' />
<div className='flex flex-[43] flex-col overflow-hidden'>
<ModelsCell />
<div className='h-7 shrink-0 border-[#E9E9E9] border-y bg-[#FDFDFD]' />
<IntegrationsCell />
</div>
<div className='w-7 shrink-0 border-[#E9E9E9] border-x bg-[#FDFDFD]' />
<div className='flex flex-[27] flex-col overflow-hidden'>
<DeployCell />
<div className='h-7 shrink-0 border-[#E9E9E9] border-y bg-[#FDFDFD]' />
<LogsCell />
</div>
</div>
</div>
</section>
)
}

View File

@@ -12,10 +12,10 @@ import {
useBlockCycle,
} from '@/app/(home)/components/hero/components/animated-blocks'
const HeroPreview = dynamic(
const LandingPreview = dynamic(
() =>
import('@/app/(home)/components/hero/components/hero-preview/hero-preview').then(
(mod) => mod.HeroPreview
import('@/app/(home)/components/landing-preview/landing-preview').then(
(mod) => mod.LandingPreview
),
{
ssr: false,
@@ -27,21 +27,6 @@ const HeroPreview = dynamic(
const CTA_BASE =
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
/**
* Hero section — above-the-fold value proposition.
*
* SEO:
* - `<section id="hero" aria-labelledby="hero-heading">`.
* - Contains the page's only `<h1>`. Text aligns with the `<title>` tag keyword.
* - Subtitle `<p>` expands the H1 into a full sentence with the primary keyword.
* - Primary CTA links to `/signup` and `/login` auth pages (crawlable).
* - Canvas/animations wrapped in `aria-hidden="true"` with a text alternative.
*
* GEO:
* - H1 + subtitle answer "What is Sim?" in two sentences (answer-first pattern).
* - First 150 chars of visible text explicitly name "Sim", "AI agents", "agentic workflows".
* - `<p className="sr-only">` product summary (~50 words) is an atomic answer for AI citation.
*/
export default function Hero() {
const blockStates = useBlockCycle()
@@ -51,7 +36,6 @@ export default function Hero() {
aria-labelledby='hero-heading'
className='relative flex flex-col items-center overflow-hidden bg-[#1C1C1C] pt-[71px]'
>
{/* Screen reader product summary */}
<p className='sr-only'>
Sim is the open-source platform to build AI agents and run your agentic workforce. Connect
1,000+ integrations and LLMs including OpenAI, Claude, Gemini, Mistral, and xAI to
@@ -60,7 +44,6 @@ export default function Hero() {
HIPAA compliant.
</p>
{/* Left card decoration — top-left, partially off-screen */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-[-0.7vw] left-[-2.8vw] z-0 aspect-[344/328] w-[23.9vw]'
@@ -68,7 +51,6 @@ export default function Hero() {
<Image src='/landing/card-left.svg' alt='' fill className='object-contain' />
</div>
{/* Right card decoration — top-right, partially off-screen */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-[-2.8vw] right-[0vw] z-0 aspect-[471/470] w-[32.7vw]'
@@ -76,7 +58,6 @@ export default function Hero() {
<Image src='/landing/card-right.svg' alt='' fill className='object-contain' />
</div>
{/* Main content */}
<div className='relative z-10 flex flex-col items-center gap-[12px]'>
<h1
id='hero-heading'
@@ -88,11 +69,10 @@ export default function Hero() {
Build and deploy agentic workflows
</p>
{/* CTA Buttons */}
<div className='mt-[12px] flex items-center gap-[8px]'>
<Link
href='/login'
className={`${CTA_BASE} border-[#2A2A2A] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
className={`${CTA_BASE} border-[#3d3d3d] text-[#ECECEC] transition-colors hover:bg-[#2A2A2A]`}
aria-label='Log in'
>
Log in
@@ -107,7 +87,6 @@ export default function Hero() {
</div>
</div>
{/* Top-right blocks */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 right-[13.1vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
@@ -115,7 +94,6 @@ export default function Hero() {
<BlocksTopRightAnimated animState={blockStates.topRight} />
</div>
{/* Top-left blocks */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-0 left-[16vw] z-20 w-[calc(140px_+_10.76vw)] max-w-[295px]'
@@ -123,9 +101,7 @@ export default function Hero() {
<BlocksTopLeftAnimated animState={blockStates.topLeft} />
</div>
{/* Product Screenshot with decorative elements */}
<div className='relative z-10 mx-auto mt-[2.4vw] w-[78.9vw] px-[1.4vw]'>
{/* Left side blocks - flush against screenshot left edge */}
<div
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'
@@ -133,7 +109,6 @@ export default function Hero() {
<BlocksLeftAnimated animState={blockStates.left} />
</div>
{/* Right side blocks - flush against screenshot right edge, mirrored to point outward */}
<div
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-[50%] left-[calc(100%-1.41vw)] z-20 w-[calc(16px_+_1.25vw)] max-w-[34px] scale-x-[-1]'
@@ -141,13 +116,11 @@ export default function Hero() {
<BlocksRightSideAnimated animState={blockStates.rightSide} />
</div>
{/* Interactive workspace preview */}
<div className='relative z-10 overflow-hidden rounded border border-[#2A2A2A]'>
<HeroPreview />
<LandingPreview />
</div>
</div>
{/* Right edge blocks - at right edge of screen */}
<div
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-[50%] right-0 z-20 w-[calc(16px_+_1.25vw)] max-w-[34px]'

View File

@@ -17,7 +17,7 @@ import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
* aside > div.border-l.pt-[14px] > Header(px-8) > Tabs(px-8,pt-14) > Content(pt-12)
* inside Content > Copilot > header-bar(mx-[-1px]) > UserInput(p-8)
*/
export const HeroPreviewPanel = memo(function HeroPreviewPanel() {
export const LandingPreviewPanel = memo(function LandingPreviewPanel() {
const router = useRouter()
const [inputValue, setInputValue] = useState('')
const textareaRef = useRef<HTMLTextAreaElement>(null)
@@ -85,10 +85,7 @@ export const HeroPreviewPanel = memo(function HeroPreviewPanel() {
<div className='h-full w-[8px] bg-[#FA4EDF]' />
<div className='h-full w-[14px] bg-[#FA4EDF] opacity-60' />
</div>
<div
className='flex items-center gap-[5px] bg-white px-[6px] py-[4px] font-medium text-[#1C1C1C] text-[11px]'
style={{ boxShadow: '2px 2px 0px 0px #3d3d3d' }}
>
<div className='flex items-center gap-[5px] bg-white px-[6px] py-[4px] font-medium text-[#1C1C1C] text-[11px]'>
Get started
<ChevronDown className='-rotate-90 h-[7px] w-[7px] text-[#1C1C1C]' />
</div>

View File

@@ -3,12 +3,12 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { Database, Layout, Search, Settings } from 'lucide-react'
import { ChevronDown, Library } from '@/components/emcn'
import type { PreviewWorkflow } from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-workflow/workflow-data'
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/**
* Props for the HeroPreviewSidebar component
* Props for the LandingPreviewSidebar component
*/
interface HeroPreviewSidebarProps {
interface LandingPreviewSidebarProps {
workflows: PreviewWorkflow[]
activeWorkflowId: string
onSelectWorkflow: (id: string) => void
@@ -32,11 +32,11 @@ const FOOTER_NAV_ITEMS = [
* --surface-1: #1e1e1e, --surface-5: #363636, --border: #2c2c2c, --border-1: #3d3d3d
* --text-primary: #e6e6e6, --text-tertiary: #b3b3b3, --text-muted: #787878
*/
export function HeroPreviewSidebar({
export function LandingPreviewSidebar({
workflows,
activeWorkflowId,
onSelectWorkflow,
}: HeroPreviewSidebarProps) {
}: LandingPreviewSidebarProps) {
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

View File

@@ -16,16 +16,22 @@ import ReactFlow, {
ReactFlowProvider,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { PreviewBlockNode } from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-workflow/preview-block-node'
import { PreviewBlockNode } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/preview-block-node'
import {
EASE_OUT,
type PreviewWorkflow,
toReactFlowElements,
} from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-workflow/workflow-data'
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
interface HeroPreviewWorkflowProps {
interface FitViewOptions {
padding?: number
maxZoom?: number
}
interface LandingPreviewWorkflowProps {
workflow: PreviewWorkflow
animate?: boolean
fitViewOptions?: FitViewOptions
}
/**
@@ -82,13 +88,13 @@ function PreviewEdge({
const NODE_TYPES: NodeTypes = { previewBlock: PreviewBlockNode }
const EDGE_TYPES: EdgeTypes = { previewEdge: PreviewEdge }
const PRO_OPTIONS = { hideAttribution: true }
const FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1 } as const
const DEFAULT_FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1 } as const
/**
* Inner flow component. Keyed on workflow ID by the parent so it remounts
* cleanly on workflow switch fitView fires on mount with zero delay.
*/
function PreviewFlow({ workflow, animate = false }: HeroPreviewWorkflowProps) {
function PreviewFlow({ workflow, animate = false, fitViewOptions }: LandingPreviewWorkflowProps) {
const { nodes: initialNodes, edges: initialEdges } = useMemo(
() => toReactFlowElements(workflow, animate),
[workflow, animate]
@@ -107,6 +113,8 @@ function PreviewFlow({ workflow, animate = false }: HeroPreviewWorkflowProps) {
[]
)
const resolvedFitViewOptions = fitViewOptions ?? DEFAULT_FIT_VIEW_OPTIONS
return (
<ReactFlow
nodes={nodes}
@@ -128,7 +136,7 @@ function PreviewFlow({ workflow, animate = false }: HeroPreviewWorkflowProps) {
autoPanOnNodeDrag={false}
proOptions={PRO_OPTIONS}
fitView
fitViewOptions={FIT_VIEW_OPTIONS}
fitViewOptions={resolvedFitViewOptions}
className='h-full w-full bg-[#1b1b1b]'
/>
)
@@ -139,11 +147,15 @@ function PreviewFlow({ workflow, animate = false }: HeroPreviewWorkflowProps) {
* The key on workflow.id forces a clean remount on switch instant fitView,
* no timers, no flicker.
*/
export function HeroPreviewWorkflow({ workflow, animate = false }: HeroPreviewWorkflowProps) {
export function LandingPreviewWorkflow({
workflow,
animate = false,
fitViewOptions,
}: LandingPreviewWorkflowProps) {
return (
<div className='h-full w-full'>
<ReactFlowProvider key={workflow.id}>
<PreviewFlow workflow={workflow} animate={animate} />
<PreviewFlow workflow={workflow} animate={animate} fitViewOptions={fitViewOptions} />
</ReactFlowProvider>
</div>
)

View File

@@ -6,11 +6,29 @@ import { Database } from 'lucide-react'
import { Handle, type NodeProps, Position } from 'reactflow'
import {
AgentIcon,
AnthropicIcon,
FirecrawlIcon,
GeminiIcon,
GithubIcon,
GmailIcon,
GoogleCalendarIcon,
GoogleSheetsIcon,
JiraIcon,
LinearIcon,
LinkedInIcon,
MistralIcon,
NotionIcon,
OpenAIIcon,
RedditIcon,
ReductoIcon,
ScheduleIcon,
SlackIcon,
StartIcon,
SupabaseIcon,
TelegramIcon,
TextractIcon,
WebhookIcon,
xAIIcon,
xIcon,
YouTubeIcon,
} from '@/components/icons'
@@ -18,7 +36,7 @@ import {
BLOCK_STAGGER,
EASE_OUT,
type PreviewTool,
} from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-workflow/workflow-data'
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/** Map block type strings to their icon components. */
const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -32,6 +50,39 @@ const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> =
schedule: ScheduleIcon,
telegram: TelegramIcon,
knowledge_base: Database,
webhook: WebhookIcon,
github: GithubIcon,
supabase: SupabaseIcon,
google_calendar: GoogleCalendarIcon,
gmail: GmailIcon,
google_sheets: GoogleSheetsIcon,
linear: LinearIcon,
firecrawl: FirecrawlIcon,
reddit: RedditIcon,
notion: NotionIcon,
reducto: ReductoIcon,
textract: TextractIcon,
linkedin: LinkedInIcon,
}
/** Model prefix → provider icon for the "Model" row in agent blocks. */
const MODEL_PROVIDER_ICONS: Array<{
prefix: string
icon: React.ComponentType<{ className?: string }>
size?: string
}> = [
{ prefix: 'gpt-', icon: OpenAIIcon },
{ prefix: 'o3', icon: OpenAIIcon },
{ prefix: 'o4', icon: OpenAIIcon },
{ prefix: 'claude-', icon: AnthropicIcon },
{ prefix: 'gemini-', icon: GeminiIcon },
{ prefix: 'grok-', icon: xAIIcon, size: 'h-[17px] w-[17px]' },
{ prefix: 'mistral-', icon: MistralIcon },
]
function getModelIconEntry(modelValue: string) {
const lower = modelValue.toLowerCase()
return MODEL_PROVIDER_ICONS.find((m) => lower.startsWith(m.prefix)) ?? null
}
/**
@@ -145,23 +196,32 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
{/* Sub-block rows + tools */}
{hasContent && (
<div className='flex flex-col gap-[8px] p-[8px]'>
{rows.map((row) => (
<div key={row.title} className='flex items-center gap-[8px]'>
<span className='min-w-0 truncate text-[#b3b3b3] text-[14px] capitalize'>
{row.title}
</span>
{row.value && (
<span className='flex-1 truncate text-right text-[#e6e6e6] text-[14px]'>
{row.value}
{rows.map((row) => {
const modelEntry = row.title === 'Model' ? getModelIconEntry(row.value) : null
const ModelIcon = modelEntry?.icon
return (
<div key={row.title} className='flex items-center gap-[8px]'>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px] capitalize'>
{row.title}
</span>
)}
</div>
))}
{row.value && (
<span className='flex min-w-0 flex-1 items-center justify-end gap-[5px] font-normal text-[#e6e6e6] text-[14px]'>
{ModelIcon && (
<ModelIcon
className={`inline-block flex-shrink-0 text-[#e6e6e6] ${modelEntry.size ?? 'h-[14px] w-[14px]'}`}
/>
)}
<span className='truncate'>{row.value}</span>
</span>
)}
</div>
)
})}
{/* Tool chips — inline with label */}
{tools && tools.length > 0 && (
<div className='flex items-center gap-[8px]'>
<span className='flex-shrink-0 text-[#b3b3b3] text-[14px]'>Tools</span>
<span className='flex-shrink-0 font-normal text-[#b3b3b3] text-[14px]'>Tools</span>
<div className='flex flex-1 flex-wrap items-center justify-end gap-[5px]'>
{tools.map((tool) => {
const ToolIcon = BLOCK_ICONS[tool.type]
@@ -176,7 +236,7 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
>
{ToolIcon && <ToolIcon className='h-[10px] w-[10px] text-white' />}
</div>
<span className='text-[#e6e6e6] text-[12px]'>{tool.name}</span>
<span className='font-normal text-[#e6e6e6] text-[12px]'>{tool.name}</span>
</div>
)
})}

View File

@@ -43,7 +43,7 @@ export interface PreviewWorkflow {
const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
id: 'wf-it-service',
name: 'IT Service Management',
color: '#33C482',
color: '#FF6B2C',
blocks: [
{
id: 'slack-1',
@@ -84,7 +84,7 @@ const IT_SERVICE_WORKFLOW: PreviewWorkflow = {
const CONTENT_PIPELINE_WORKFLOW: PreviewWorkflow = {
id: 'wf-content-pipeline',
name: 'Content Pipeline',
color: '#FF6B2C',
color: '#33C482',
blocks: [
{
id: 'schedule-1',
@@ -147,7 +147,7 @@ const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
type: 'note',
bgColor: 'transparent',
rows: [],
markdown: '### What will you build?\n\n_"Find Slack todos and add them to Linear"_',
markdown: '### What will you build?\n\n_"Find Linear todos and send in Slack"_',
position: { x: 0, y: 0 },
hideTargetHandle: true,
hideSourceHandle: true,
@@ -157,8 +157,8 @@ const NEW_AGENT_WORKFLOW: PreviewWorkflow = {
}
export const PREVIEW_WORKFLOWS: PreviewWorkflow[] = [
IT_SERVICE_WORKFLOW,
CONTENT_PIPELINE_WORKFLOW,
IT_SERVICE_WORKFLOW,
NEW_AGENT_WORKFLOW,
]

View File

@@ -2,13 +2,13 @@
import { useEffect, useRef, useState } from 'react'
import { motion, type Variants } from 'framer-motion'
import { HeroPreviewPanel } from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-panel/hero-preview-panel'
import { HeroPreviewSidebar } from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-sidebar/hero-preview-sidebar'
import { HeroPreviewWorkflow } from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-workflow/hero-preview-workflow'
import { LandingPreviewPanel } from '@/app/(home)/components/landing-preview/components/landing-preview-panel/landing-preview-panel'
import { LandingPreviewSidebar } from '@/app/(home)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
import { LandingPreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
import {
EASE_OUT,
PREVIEW_WORKFLOWS,
} from '@/app/(home)/components/hero/components/hero-preview/components/hero-preview-workflow/workflow-data'
} from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
const containerVariants: Variants = {
hidden: {},
@@ -55,7 +55,7 @@ const panelVariants: Variants = {
* staggered fade. Edges draw left-to-right. Animations only fire on initial
* load workflow switches render instantly.
*/
export function HeroPreview() {
export function LandingPreview() {
const [activeWorkflowId, setActiveWorkflowId] = useState(PREVIEW_WORKFLOWS[0].id)
const isInitialMount = useRef(true)
@@ -74,17 +74,17 @@ export function HeroPreview() {
variants={containerVariants}
>
<motion.div className='hidden lg:flex' variants={sidebarVariants}>
<HeroPreviewSidebar
<LandingPreviewSidebar
workflows={PREVIEW_WORKFLOWS}
activeWorkflowId={activeWorkflowId}
onSelectWorkflow={setActiveWorkflowId}
/>
</motion.div>
<div className='relative flex-1 overflow-hidden'>
<HeroPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
<LandingPreviewWorkflow workflow={activeWorkflow} animate={isInitialMount.current} />
</div>
<motion.div className='hidden lg:flex' variants={panelVariants}>
<HeroPreviewPanel />
<LandingPreviewPanel />
</motion.div>
</motion.div>
)

View File

@@ -23,22 +23,6 @@ const LOGO_CELL = 'flex items-center px-[20px]'
/** Links: even spacing between items. */
const LINK_CELL = 'flex items-center px-[14px]'
/**
* Landing page navigation bar.
*
* Server Component for immediate crawlability. Only the GitHubStars counter
* is a client component (hydrates independently).
*
* SEO:
* - `<nav>` with schema.org `SiteNavigationElement`.
* - All routes use `<Link>` (crawlable). External links include `rel="noopener noreferrer"`.
* - Logo `<Image>` uses `priority` (LCP element).
*
* GEO:
* - Navigation items in semantic `<ul>/<li>` with descriptive anchor text.
* - Descriptive `aria-label` on interactive elements for AI intent extraction.
* - Server-rendered content available immediately to AI crawlers.
*/
export default function Navbar() {
return (
<nav
@@ -95,7 +79,7 @@ export default function Navbar() {
<div className='flex items-center gap-[8px] px-[20px]'>
<Link
href='/login'
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#2A2A2A] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
aria-label='Log in'
>
Log in

View File

@@ -0,0 +1,582 @@
import type { PreviewWorkflow } from '@/app/(home)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/**
* OCR Invoice to DB — Start → Agent (Textract) → Supabase
* Pattern: Straight line (all blocks aligned at top)
*/
const OCR_INVOICE_WORKFLOW: PreviewWorkflow = {
id: 'tpl-ocr-invoice',
name: 'OCR Invoice to DB',
color: '#2ABBF8',
blocks: [
{
id: 'starter-1',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'URL', value: 'invoice.pdf' }],
position: { x: 40, y: 80 },
hideTargetHandle: true,
},
{
id: 'agent-1',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.2' },
{ title: 'System Prompt', value: 'Extract invoice fields...' },
],
tools: [{ name: 'Textract', type: 'textract', bgColor: '#055F4E' }],
position: { x: 400, y: 100 },
},
{
id: 'supabase-1',
name: 'Supabase',
type: 'supabase',
bgColor: '#1C1C1C',
rows: [
{ title: 'Table', value: 'invoices' },
{ title: 'Operation', value: 'Insert Row' },
],
position: { x: 760, y: 80 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-1', target: 'agent-1' },
{ id: 'e-2', source: 'agent-1', target: 'supabase-1' },
],
}
/**
* GitHub Release Agent — GitHub → Agent → Slack
* Pattern: Convex (low → high → low)
*/
const GITHUB_RELEASE_WORKFLOW: PreviewWorkflow = {
id: 'tpl-github-release',
name: 'GitHub Release Agent',
color: '#00F701',
blocks: [
{
id: 'github-1',
name: 'GitHub',
type: 'github',
bgColor: '#181C1E',
rows: [
{ title: 'Event', value: 'New Release' },
{ title: 'Repository', value: 'org/repo' },
],
position: { x: 60, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-2',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-sonnet-4.6' },
{ title: 'System Prompt', value: 'Summarize changelog...' },
],
position: { x: 370, y: 50 },
},
{
id: 'slack-1',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#releases' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 140 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'github-1', target: 'agent-2' },
{ id: 'e-2', source: 'agent-2', target: 'slack-1' },
],
}
/**
* Meeting Follow-up Agent — Google Calendar → Agent → Gmail
* Pattern: Concave (high → low → high)
*/
const MEETING_FOLLOWUP_WORKFLOW: PreviewWorkflow = {
id: 'tpl-meeting-followup',
name: 'Meeting Follow-up Agent',
color: '#FFCC02',
blocks: [
{
id: 'gcal-1',
name: 'Google Calendar',
type: 'google_calendar',
bgColor: '#E0E0E0',
rows: [
{ title: 'Event', value: 'Meeting Ended' },
{ title: 'Calendar', value: 'Work' },
],
position: { x: 60, y: 60 },
hideTargetHandle: true,
},
{
id: 'agent-3',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gemini-2.5-pro' },
{ title: 'System Prompt', value: 'Draft follow-up email...' },
],
position: { x: 370, y: 150 },
},
{
id: 'gmail-1',
name: 'Gmail',
type: 'gmail',
bgColor: '#E0E0E0',
rows: [
{ title: 'Operation', value: 'Send Email' },
{ title: 'To', value: 'attendees' },
],
position: { x: 680, y: 60 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'gcal-1', target: 'agent-3' },
{ id: 'e-2', source: 'agent-3', target: 'gmail-1' },
],
}
/**
* CV/Resume Scanner — Start → Agent (Reducto) → Google Sheets
* Pattern: Convex (low → high → low)
*/
const CV_SCANNER_WORKFLOW: PreviewWorkflow = {
id: 'tpl-cv-scanner',
name: 'CV/Resume Scanner',
color: '#FA4EDF',
blocks: [
{
id: 'starter-2',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'File URL', value: 'resume.pdf' }],
position: { x: 60, y: 145 },
hideTargetHandle: true,
},
{
id: 'agent-4',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-opus-4.6' },
{ title: 'System Prompt', value: 'Parse resume fields...' },
],
tools: [{ name: 'Reducto', type: 'reducto', bgColor: '#5c0c5c' }],
position: { x: 370, y: 55 },
},
{
id: 'gsheets-1',
name: 'Google Sheets',
type: 'google_sheets',
bgColor: '#E0E0E0',
rows: [
{ title: 'Spreadsheet', value: 'Candidates' },
{ title: 'Operation', value: 'Append Row' },
],
position: { x: 680, y: 145 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-2', target: 'agent-4' },
{ id: 'e-2', source: 'agent-4', target: 'gsheets-1' },
],
}
/**
* Email Triage Agent — Gmail → Agent (KB) → fan-out to Slack + Linear
* Pattern: Fan-out (input low → agent mid → outputs spread vertically)
*/
const EMAIL_TRIAGE_WORKFLOW: PreviewWorkflow = {
id: 'tpl-email-triage',
name: 'Email Triage Agent',
color: '#FF6B2C',
blocks: [
{
id: 'gmail-2',
name: 'Gmail',
type: 'gmail',
bgColor: '#E0E0E0',
rows: [
{ title: 'Event', value: 'New Email' },
{ title: 'Label', value: 'Inbox' },
],
position: { x: 60, y: 130 },
hideTargetHandle: true,
},
{
id: 'agent-5',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.2-mini' },
{ title: 'System Prompt', value: 'Classify and route...' },
],
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#00B0B0' }],
position: { x: 370, y: 100 },
},
{
id: 'slack-2',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#urgent' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 20 },
hideSourceHandle: true,
},
{
id: 'linear-1',
name: 'Linear',
type: 'linear',
bgColor: '#5E6AD2',
rows: [
{ title: 'Project', value: 'Support' },
{ title: 'Operation', value: 'Create Issue' },
],
position: { x: 680, y: 200 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'gmail-2', target: 'agent-5' },
{ id: 'e-2', source: 'agent-5', target: 'slack-2' },
{ id: 'e-3', source: 'agent-5', target: 'linear-1' },
],
}
/**
* Competitor Monitor — Schedule → Agent (Firecrawl) → Slack
* Pattern: Concave (high → low → high)
*/
const COMPETITOR_MONITOR_WORKFLOW: PreviewWorkflow = {
id: 'tpl-competitor-monitor',
name: 'Competitor Monitor',
color: '#6366F1',
blocks: [
{
id: 'schedule-1',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [
{ title: 'Run Frequency', value: 'Daily' },
{ title: 'Time', value: '08:00 AM' },
],
position: { x: 60, y: 50 },
hideTargetHandle: true,
},
{
id: 'agent-6',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'grok-4' },
{ title: 'System Prompt', value: 'Monitor competitor...' },
],
tools: [{ name: 'Firecrawl', type: 'firecrawl', bgColor: '#181C1E' }],
position: { x: 370, y: 150 },
},
{
id: 'slack-3',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#competitive-intel' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 50 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'schedule-1', target: 'agent-6' },
{ id: 'e-2', source: 'agent-6', target: 'slack-3' },
],
}
/**
* Social Listening Agent — Schedule → Agent (Reddit + X) → Notion
* Pattern: Convex (low → high → low)
*/
const SOCIAL_LISTENING_WORKFLOW: PreviewWorkflow = {
id: 'tpl-social-listening',
name: 'Social Listening Agent',
color: '#F43F5E',
blocks: [
{
id: 'schedule-2',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [{ title: 'Run Frequency', value: 'Hourly' }],
position: { x: 60, y: 150 },
hideTargetHandle: true,
},
{
id: 'agent-7',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gemini-2.5-flash' },
{ title: 'System Prompt', value: 'Track brand mentions...' },
],
tools: [
{ name: 'Reddit', type: 'reddit', bgColor: '#FF5700' },
{ name: 'X', type: 'x', bgColor: '#000000' },
],
position: { x: 370, y: 55 },
},
{
id: 'notion-1',
name: 'Notion',
type: 'notion',
bgColor: '#181C1E',
rows: [
{ title: 'Database', value: 'Brand Mentions' },
{ title: 'Operation', value: 'Create Page' },
],
position: { x: 680, y: 150 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'schedule-2', target: 'agent-7' },
{ id: 'e-2', source: 'agent-7', target: 'notion-1' },
],
}
/**
* Data Enrichment Pipeline — Start → Agent (LinkedIn) → Google Sheets
* Pattern: Concave (high → low → high)
*/
const DATA_ENRICHMENT_WORKFLOW: PreviewWorkflow = {
id: 'tpl-data-enrichment',
name: 'Data Enrichment Pipeline',
color: '#14B8A6',
blocks: [
{
id: 'starter-3',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'Email', value: 'lead@company.com' }],
position: { x: 60, y: 55 },
hideTargetHandle: true,
},
{
id: 'agent-8',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'mistral-large' },
{ title: 'System Prompt', value: 'Enrich lead data...' },
],
tools: [{ name: 'LinkedIn', type: 'linkedin', bgColor: '#0072B1' }],
position: { x: 370, y: 145 },
},
{
id: 'gsheets-2',
name: 'Google Sheets',
type: 'google_sheets',
bgColor: '#E0E0E0',
rows: [
{ title: 'Spreadsheet', value: 'Lead Database' },
{ title: 'Operation', value: 'Update Row' },
],
position: { x: 680, y: 55 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-3', target: 'agent-8' },
{ id: 'e-2', source: 'agent-8', target: 'gsheets-2' },
],
}
/**
* Customer Feedback Digest — Schedule → Agent → Slack
* Pattern: Convex (low → high → low)
*/
const FEEDBACK_DIGEST_WORKFLOW: PreviewWorkflow = {
id: 'tpl-feedback-digest',
name: 'Customer Feedback Digest',
color: '#F59E0B',
blocks: [
{
id: 'schedule-3',
name: 'Schedule',
type: 'schedule',
bgColor: '#6366F1',
rows: [
{ title: 'Run Frequency', value: 'Daily' },
{ title: 'Time', value: '09:00 AM' },
],
position: { x: 60, y: 145 },
hideTargetHandle: true,
},
{
id: 'agent-9',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'claude-sonnet-4.6' },
{ title: 'System Prompt', value: 'Analyze customer feedback...' },
],
tools: [{ name: 'Airtable', type: 'airtable', bgColor: '#18BFFF' }],
position: { x: 370, y: 50 },
},
{
id: 'slack-4',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#product-feedback' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 145 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'schedule-3', target: 'agent-9' },
{ id: 'e-2', source: 'agent-9', target: 'slack-4' },
],
}
/**
* PR Review Agent — GitHub → Agent → Slack
* Pattern: Concave (high → low → high)
*/
const PR_REVIEW_WORKFLOW: PreviewWorkflow = {
id: 'tpl-pr-review',
name: 'PR Review Agent',
color: '#06B6D4',
blocks: [
{
id: 'github-2',
name: 'GitHub',
type: 'github',
bgColor: '#181C1E',
rows: [
{ title: 'Event', value: 'Pull Request Opened' },
{ title: 'Repository', value: 'org/repo' },
],
position: { x: 60, y: 60 },
hideTargetHandle: true,
},
{
id: 'agent-10',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gpt-5.2' },
{ title: 'System Prompt', value: 'Review code changes...' },
],
position: { x: 370, y: 155 },
},
{
id: 'slack-5',
name: 'Slack',
type: 'slack',
bgColor: '#611f69',
rows: [
{ title: 'Channel', value: '#code-reviews' },
{ title: 'Operation', value: 'Send Message' },
],
position: { x: 680, y: 60 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'github-2', target: 'agent-10' },
{ id: 'e-2', source: 'agent-10', target: 'slack-5' },
],
}
/**
* Knowledge Base QA — Start → Agent (KB) → Response
* Pattern: Convex (low → high → low)
*/
const KNOWLEDGE_QA_WORKFLOW: PreviewWorkflow = {
id: 'tpl-knowledge-qa',
name: 'Knowledge Base QA',
color: '#84CC16',
blocks: [
{
id: 'starter-4',
name: 'Start',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'Question', value: 'How do I...' }],
position: { x: 60, y: 140 },
hideTargetHandle: true,
},
{
id: 'agent-11',
name: 'Agent',
type: 'agent',
bgColor: '#701ffc',
rows: [
{ title: 'Model', value: 'gemini-2.5-pro' },
{ title: 'System Prompt', value: 'Answer using knowledge...' },
],
tools: [{ name: 'Knowledge Base', type: 'knowledge_base', bgColor: '#00B0B0' }],
position: { x: 370, y: 50 },
},
{
id: 'starter-5',
name: 'Response',
type: 'starter',
bgColor: '#34B5FF',
rows: [{ title: 'Answer', value: 'Based on your docs...' }],
position: { x: 680, y: 140 },
hideSourceHandle: true,
},
],
edges: [
{ id: 'e-1', source: 'starter-4', target: 'agent-11' },
{ id: 'e-2', source: 'agent-11', target: 'starter-5' },
],
}
export const TEMPLATE_WORKFLOWS: PreviewWorkflow[] = [
OCR_INVOICE_WORKFLOW,
GITHUB_RELEASE_WORKFLOW,
MEETING_FOLLOWUP_WORKFLOW,
CV_SCANNER_WORKFLOW,
EMAIL_TRIAGE_WORKFLOW,
COMPETITOR_MONITOR_WORKFLOW,
SOCIAL_LISTENING_WORKFLOW,
DATA_ENRICHMENT_WORKFLOW,
FEEDBACK_DIGEST_WORKFLOW,
PR_REVIEW_WORKFLOW,
KNOWLEDGE_QA_WORKFLOW,
]

View File

@@ -1,55 +1,452 @@
'use client'
import { useRef } from 'react'
import { motion, useScroll, useTransform } from 'framer-motion'
import { Badge } from '@/components/emcn'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { Badge, ChevronDown } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { TEMPLATE_WORKFLOWS } from '@/app/(home)/components/templates/template-workflows'
const LandingPreviewWorkflow = dynamic(
() =>
import(
'@/app/(home)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
).then((mod) => mod.LandingPreviewWorkflow),
{
ssr: false,
loading: () => <div className='h-full w-full bg-[#1b1b1b]' />,
}
)
function hexToRgba(hex: string, alpha: number): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
return `rgba(${r},${g},${b},${alpha})`
}
const LEFT_WALL_CLIP = 'polygon(0 8px, 100% 0, 100% 100%, 0 100%)'
const BOTTOM_WALL_CLIP = 'polygon(0 0, 100% 0, calc(100% - 8px) 100%, 0 100%)'
interface DepthConfig {
color: string
segments: readonly (readonly [opacity: number, width: number])[]
}
/** Depth color and gradient segment pattern per template. Segments are `[opacity, width%]` tuples. */
const DEPTH_CONFIGS: Record<string, DepthConfig> = {
'tpl-ocr-invoice': {
color: '#2ABBF8',
segments: [
[0.3, 10],
[0.5, 8],
[0.8, 6],
[1, 5],
[0.4, 12],
[0.7, 8],
[1, 6],
[0.5, 10],
[0.9, 7],
[0.6, 12],
[1, 8],
[0.35, 8],
],
},
'tpl-github-release': {
color: '#00F701',
segments: [
[0.4, 8],
[0.7, 6],
[1, 5],
[0.5, 14],
[0.85, 8],
[0.3, 12],
[1, 6],
[0.6, 10],
[0.9, 7],
[0.45, 8],
[1, 8],
[0.7, 8],
],
},
'tpl-meeting-followup': {
color: '#FFCC02',
segments: [
[0.5, 12],
[0.8, 6],
[0.35, 10],
[1, 5],
[0.6, 8],
[0.9, 7],
[0.4, 14],
[1, 6],
[0.7, 10],
[0.5, 8],
[1, 6],
[0.3, 8],
],
},
'tpl-cv-scanner': {
color: '#FA4EDF',
segments: [
[0.35, 6],
[0.6, 10],
[0.9, 5],
[1, 6],
[0.4, 8],
[0.75, 12],
[0.5, 7],
[1, 5],
[0.3, 10],
[0.8, 8],
[0.6, 9],
[1, 6],
[0.45, 8],
],
},
'tpl-email-triage': {
color: '#FF6B2C',
segments: [
[0.4, 10],
[0.7, 8],
[1, 5],
[0.5, 12],
[0.85, 6],
[0.3, 10],
[1, 6],
[0.6, 8],
[0.9, 7],
[0.4, 12],
[1, 8],
[0.65, 8],
],
},
'tpl-competitor-monitor': {
color: '#6366F1',
segments: [
[0.3, 8],
[0.55, 10],
[0.8, 6],
[1, 5],
[0.4, 12],
[0.7, 7],
[0.9, 8],
[0.5, 10],
[1, 6],
[0.35, 8],
[0.75, 6],
[1, 6],
[0.6, 8],
],
},
'tpl-social-listening': {
color: '#F43F5E',
segments: [
[0.5, 10],
[0.8, 6],
[0.4, 8],
[1, 5],
[0.6, 12],
[0.35, 8],
[0.9, 7],
[1, 6],
[0.5, 10],
[0.75, 8],
[0.4, 6],
[1, 6],
[0.65, 8],
],
},
'tpl-data-enrichment': {
color: '#14B8A6',
segments: [
[0.35, 8],
[0.6, 6],
[0.9, 5],
[0.4, 12],
[1, 6],
[0.7, 10],
[0.5, 7],
[0.85, 8],
[1, 5],
[0.3, 10],
[0.65, 8],
[1, 7],
[0.5, 8],
],
},
'tpl-feedback-digest': {
color: '#F59E0B',
segments: [
[0.4, 10],
[0.65, 6],
[0.9, 5],
[0.5, 12],
[1, 6],
[0.35, 8],
[0.75, 7],
[1, 5],
[0.6, 10],
[0.85, 8],
[0.45, 6],
[1, 8],
[0.55, 9],
],
},
'tpl-pr-review': {
color: '#06B6D4',
segments: [
[0.35, 8],
[0.7, 7],
[1, 5],
[0.45, 10],
[0.8, 6],
[0.3, 12],
[1, 6],
[0.55, 8],
[0.9, 7],
[0.4, 10],
[1, 6],
[0.65, 8],
[0.5, 7],
],
},
'tpl-knowledge-qa': {
color: '#84CC16',
segments: [
[0.5, 8],
[0.75, 6],
[0.4, 10],
[1, 5],
[0.6, 8],
[0.85, 7],
[0.35, 12],
[1, 6],
[0.7, 8],
[0.45, 10],
[0.9, 6],
[1, 6],
[0.55, 8],
],
},
}
function buildBottomWallStyle(config: DepthConfig) {
let pos = 0
const stops: string[] = []
for (const [opacity, width] of config.segments) {
const c = hexToRgba(config.color, opacity)
stops.push(`${c} ${pos}%`, `${c} ${pos + width}%`)
pos += width
}
return {
clipPath: BOTTOM_WALL_CLIP,
background: `linear-gradient(135deg, ${stops.join(', ')})`,
}
}
interface DotGridProps {
className?: string
cols: number
rows: number
gap?: number
}
function DotGrid({ className, cols, rows, gap = 0 }: DotGridProps) {
return (
<div
aria-hidden='true'
className={className}
style={{
display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gap,
placeItems: 'center',
}}
>
{Array.from({ length: cols * rows }, (_, i) => (
<div key={i} className='h-[2px] w-[2px] rounded-full bg-[#2A2A2A]' />
))}
</div>
)
}
const TEMPLATES_PANEL_ID = 'templates-panel'
export default function Templates() {
const sectionRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ['start end', 'end start'],
})
const [activeIndex, setActiveIndex] = useState(0)
const inset = useTransform(scrollYProgress, [0.1, 0.35], [0, 16])
const borderRadius = useTransform(scrollYProgress, [0.1, 0.35], [0, 4])
const activeWorkflow = TEMPLATE_WORKFLOWS[activeIndex]
const activeDepth = DEPTH_CONFIGS[activeWorkflow.id]
return (
<section
ref={sectionRef}
id='templates'
aria-labelledby='templates-heading'
className='mt-[40px] bg-[#F6F6F6]'
>
<motion.div style={{ padding: inset }}>
<motion.div
style={{ borderRadius }}
className='bg-[#1C1C1C] px-[80px] pt-[120px] pb-[80px]'
>
<div className='flex flex-col items-start gap-[20px]'>
<Badge
variant='blue'
size='md'
dot
className='bg-[rgba(42,187,248,0.1)] font-season text-[#2ABBF8] uppercase tracking-[0.02em]'
>
Templates
</Badge>
<section id='templates' aria-labelledby='templates-heading' className='mt-[40px] mb-[80px]'>
<p className='sr-only'>
Sim includes {TEMPLATE_WORKFLOWS.length} pre-built workflow templates covering OCR
processing, release management, meeting follow-ups, resume scanning, email triage,
competitor monitoring, social listening, data enrichment, feedback analysis, code review,
and knowledge base Q&amp;A. Each template connects real integrations and LLMs pick one,
customise it, and deploy in minutes.
</p>
<h2
id='templates-heading'
className='font-[430] font-season text-[40px] text-white leading-[100%] tracking-[-0.02em]'
>
Ready-made AI templates.
</h2>
<div className='bg-[#1C1C1C]'>
<DotGrid
className='border-[#2A2A2A] border-y bg-[#1C1C1C] p-[6px]'
cols={120}
rows={1}
gap={6}
/>
<p className='max-w-[463px] font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
Jump-start workflows with ready-made templates for any teamfully editable for your
stack.
</p>
<div className='relative overflow-hidden'>
<div className='px-[80px] pt-[100px]'>
<div className='flex flex-col items-start gap-[20px]'>
<Badge
variant='blue'
size='md'
dot
className='font-season uppercase tracking-[0.02em] transition-colors duration-200'
style={{
color: activeDepth.color,
backgroundColor: hexToRgba(activeDepth.color, 0.1),
}}
>
Templates
</Badge>
<h2
id='templates-heading'
className='font-[430] font-season text-[40px] text-white leading-[100%] tracking-[-0.02em]'
>
Go from idea to production in minutes.
</h2>
<p className='max-w-[640px] font-[430] font-season text-[#F6F6F0]/50 text-[16px] leading-[125%] tracking-[0.02em]'>
1,000+ integrations wired into pre-built templates for every use casepick one, swap
models and tools to fit your stack, and deploy in minutes.
</p>
</div>
</div>
</motion.div>
</motion.div>
<div className='mt-[73px] flex border-[#2A2A2A] border-y'>
<DotGrid
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-r p-[6px]'
cols={6}
rows={55}
gap={6}
/>
<div className='flex min-w-0 flex-1'>
<div
role='tablist'
aria-label='Workflow templates'
className='flex w-[300px] shrink-0 flex-col border-[#2A2A2A] border-r'
>
{TEMPLATE_WORKFLOWS.map((workflow, index) => {
const isActive = index === activeIndex
return (
<button
key={workflow.id}
id={`template-tab-${index}`}
type='button'
role='tab'
aria-selected={isActive}
aria-controls={TEMPLATES_PANEL_ID}
onClick={() => setActiveIndex(index)}
className={cn(
'relative text-left',
isActive
? 'z-10'
: 'flex items-center px-[12px] py-[10px] shadow-[inset_0_-1px_0_0_#2A2A2A] last:shadow-none hover:bg-[#232323]/50'
)}
>
{isActive ? (
(() => {
const depth = DEPTH_CONFIGS[workflow.id]
return (
<>
<div
className='absolute top-[-8px] bottom-0 left-0 w-2'
style={{
clipPath: LEFT_WALL_CLIP,
backgroundColor: hexToRgba(depth.color, 0.63),
}}
/>
<div
className='absolute right-[-8px] bottom-0 left-2 h-2'
style={buildBottomWallStyle(depth)}
/>
<div className='-translate-y-2 relative flex translate-x-2 items-center bg-[#242424] px-[12px] py-[10px] shadow-[inset_0_0_0_1.5px_#3E3E3E]'>
<span className='flex-1 font-[430] font-season text-[16px] text-white'>
{workflow.name}
</span>
<ChevronDown
className='-rotate-90 h-[11px] w-[11px] shrink-0'
style={{ color: depth.color }}
/>
</div>
</>
)
})()
) : (
<span className='font-[430] font-season text-[#F6F6F0]/50 text-[16px]'>
{workflow.name}
</span>
)}
</button>
)
})}
</div>
<div
id={TEMPLATES_PANEL_ID}
role='tabpanel'
aria-labelledby={`template-tab-${activeIndex}`}
className='relative hidden flex-1 lg:block'
>
<div aria-hidden='true' className='h-full'>
<LandingPreviewWorkflow
key={activeIndex}
workflow={activeWorkflow}
animate
fitViewOptions={{ padding: 0.15, maxZoom: 1.3 }}
/>
</div>
<Link
href='/signup'
className='group/cta absolute top-[16px] right-[16px] z-10 inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#33C482] bg-[#33C482] px-[10px] font-[430] font-season text-[14px] text-black transition-[filter] hover:brightness-110'
>
Use template
<span className='relative h-[10px] w-[10px] shrink-0'>
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
<svg
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
viewBox='0 0 10 10'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M1 5H8M5.5 2L8.5 5L5.5 8'
stroke='currentColor'
strokeWidth='1.33'
strokeLinecap='square'
strokeLinejoin='miter'
fill='none'
/>
</svg>
</span>
</Link>
</div>
</div>
<DotGrid
className='w-[80px] shrink-0 overflow-hidden border-[#2A2A2A] border-l p-[6px]'
cols={6}
rows={55}
gap={6}
/>
</div>
</div>
</div>
</section>
)
}