chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,6 @@
export {
PricingCard,
type PricingCardCta,
type PricingCardProps,
type PricingCardSection,
} from './pricing-card'
@@ -0,0 +1,138 @@
import { Check, ChipLink, ChipTag, cn } from '@sim/emcn'
import { SlackIcon } from '@/components/icons'
import type { CellValue } from '@/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data'
/** Maps a cell-icon identifier to its brand icon component. */
const CELL_ICONS = { slack: SlackIcon } as const
/** Resolved CTA for a pricing card - label, chip variant, and destination href. */
export interface PricingCardCta {
label: string
variant: 'primary' | 'border-shadow'
href: string
}
/** A labelled group of feature rows for one plan, transposed from the comparison data. */
export interface PricingCardSection {
/** Stable key for the section, used for React reconciliation. */
key: string
/** Section header, e.g. `"Rate limits (runs/min)"`. Omit to render the rows without a header. */
title?: string
/** Feature rows - this plan's value for each comparison row in the section. */
rows: { label: string; value: CellValue }[]
}
/**
* Props for {@link PricingCard}.
*/
export interface PricingCardProps {
/** Plan name, e.g. `"Pro"`. */
name: string
/** Headline price, e.g. `"$25"`, `"$0"`, or `"Custom"`. */
price: string
/** Small line under the price, e.g. `"per user/month, billed monthly"`. */
priceSubtext?: string
/** Optional discount pill next to the price, e.g. `"15% off"`. */
discountLabel?: string
/** Resolved CTA - label, variant, and destination href. */
cta: PricingCardCta
/** Full feature breakdown for this plan, grouped by section. */
sections: PricingCardSection[]
/** Extra outer classes. */
className?: string
}
/**
* Renders one plan's value for a feature row: `true` → check, `false` → em-dash,
* an icon reference → its brand icon, a string → right-aligned text.
*/
function FeatureValue({ value }: { value: CellValue }) {
if (value === true) {
return <Check className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
}
if (value === false) {
return <span className='select-none text-[var(--text-muted)]'></span>
}
if (typeof value === 'object') {
const Icon = CELL_ICONS[value.icon]
return <Icon className='size-[14px] flex-shrink-0' />
}
return (
<span className='whitespace-nowrap text-right text-[var(--text-primary)] text-sm tabular-nums'>
{value}
</span>
)
}
/**
* Public pricing card - one plan rendered as a self-contained spec sheet: the
* header (name, price, CTA, segment) above the full comparison breakdown, grouped
* by section with a hairline rule under each section header.
*
* The radius (`rounded-lg`) matches the landing hero's visual panel so the cards
* line up with the rest of the landing chrome. Every card carries the identical
* row structure (only values differ) so a grid of them renders at exactly the
* same height. Feature rows are transposed from the shared comparison data, so
* the card can never drift from the platform.
*/
export function PricingCard({
name,
price,
priceSubtext,
discountLabel,
cta,
sections,
className,
}: PricingCardProps) {
return (
<article
className={cn(
'flex h-full flex-col gap-[22px] rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)] p-5',
className
)}
>
<div className='flex flex-col gap-[22px]'>
<h2 className='text-[24px] text-[var(--text-primary)]'>{name}</h2>
<div className='flex flex-col'>
<div className='flex items-center gap-2'>
<span className='text-[20px] text-[var(--text-primary)] tabular-nums'>{price}</span>
{discountLabel && <ChipTag variant='mono'>{discountLabel}</ChipTag>}
</div>
<p className='text-[var(--text-muted)] text-base'>{priceSubtext ?? ' '}</p>
</div>
<ChipLink
href={cta.href}
variant={cta.variant}
fullWidth
flush
className='w-full justify-center'
>
{cta.label}
</ChipLink>
</div>
<div className='flex flex-col gap-5'>
{sections.map((section) => (
<div key={section.key} className='flex flex-col'>
{section.title && (
<>
<span className='text-[var(--text-primary)] text-small'>{section.title}</span>
<div className='mt-2 mb-2.5 h-px bg-[var(--border)]' />
</>
)}
<div className='flex flex-col gap-2.5'>
{section.rows.map((row) => (
<div key={row.label} className='flex items-center justify-between gap-3'>
<span className='text-[var(--text-body)] text-sm'>{row.label}</span>
<FeatureValue value={row.value} />
</div>
))}
</div>
</div>
))}
</div>
</article>
)
}
@@ -0,0 +1 @@
export { PricingPlans, type PricingPlansProps } from './pricing-plans'
@@ -0,0 +1,154 @@
'use client'
import type { ReactNode } from 'react'
import { useQueryStates } from 'nuqs'
import { getUpgradeCardCta, type PlanTier, type UpgradeCardId } from '@/lib/billing/client'
import { ANNUAL_DISCOUNT_RATE, CREDIT_TIERS } from '@/lib/billing/constants'
import { DEMO_HREF, SIGNUP_HREF } from '@/app/(landing)/constants'
import {
PricingCard,
type PricingCardCta,
type PricingCardSection,
} from '@/app/(landing)/pricing/components/pricing-card'
import { pricingParsers, pricingUrlKeys } from '@/app/(landing)/pricing/search-params'
import { BillingPeriodToggle } from '@/app/workspace/[workspaceId]/upgrade/components'
import {
type CellValue,
COMPARISON_SECTIONS,
} from '@/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data'
/**
* A public visitor has no subscription, so each card's canonical label and
* variant resolves against the `free` tier. On this page every self-serve CTA
* funnels to sign-up regardless of plan; the enterprise CTA routes to the
* demo-request form instead.
*/
const VISITOR_TIER: PlanTier = 'free'
/** This section's rows render on the public cards without their group header. */
const HEADERLESS_SECTION_TITLE = 'Credits & pricing'
/**
* Props for {@link PricingPlans}.
*/
export interface PricingPlansProps {
/**
* Server-rendered heading slot - the page `<h1>` and its sr-only GEO summary.
* Passed as a slot so the crawlable copy stays in the server payload.
*/
heading: ReactNode
}
/**
* Transpose the shared comparison data into one plan column's feature sections,
* so each card carries the full breakdown for its plan without duplicating it.
*/
function sectionsForColumn(col: number): PricingCardSection[] {
return COMPARISON_SECTIONS.map((section) => ({
key: section.title,
title: section.title === HEADERLESS_SECTION_TITLE ? undefined : section.title,
rows: section.rows.map((row) => ({ label: row.label, value: row.values[col] as CellValue })),
}))
}
/** The four card columns (Free, Pro, Max, Enterprise) transposed once at module load - the source data is static, so this never needs to be redone per render. */
const SECTIONS_BY_COLUMN = [0, 1, 2, 3].map(sectionsForColumn)
/** Round a dollar amount down to the annual-billing discounted price. */
function annualPrice(dollars: number): number {
return Math.round(dollars * (1 - ANNUAL_DISCOUNT_RATE))
}
/**
* Resolve a card's canonical label and variant from the free-tier matrix. A
* `sales` intent ("Talk to sales") routes to the demo-request form, matching
* every other "Contact sales" CTA on the landing site; every other intent
* funnels logged-out visitors to sign-up.
*/
function resolveCta(card: UpgradeCardId): PricingCardCta {
const cta = getUpgradeCardCta(VISITOR_TIER, card)
return {
label: cta.label,
variant: cta.variant,
href: cta.intent === 'sales' ? DEMO_HREF : SIGNUP_HREF,
}
}
const FREE_CTA: PricingCardCta = {
label: 'Get started',
variant: 'border-shadow',
href: SIGNUP_HREF,
}
/**
* The single interactive island of the public pricing page. It owns the lone
* piece of state - `isAnnual`, defaulting to monthly - which the billing-period
* toggle (centered under the heading) and the Pro/Max card prices read from one
* source.
*
* Each plan (Free, Pro, Max, Enterprise) renders as a full {@link PricingCard}
* spec sheet in a four-up grid that renders at exactly the same height. Prices
* and the discount come from {@link CREDIT_TIERS} / {@link ANNUAL_DISCOUNT_RATE},
* the feature breakdown is transposed from the shared `COMPARISON_SECTIONS`, and
* the CTA labels/variants from {@link getUpgradeCardCta}. Every CTA, Free
* included, funnels to `/signup`.
*/
export function PricingPlans({ heading }: PricingPlansProps) {
const [{ billing }, setParams] = useQueryStates(pricingParsers, pricingUrlKeys)
const isAnnual = billing === 'annual'
const setIsAnnual = (next: boolean) => setParams({ billing: next ? 'annual' : 'monthly' })
const discountPct = Math.round(ANNUAL_DISCOUNT_RATE * 100)
const proPrice = isAnnual ? annualPrice(CREDIT_TIERS[0].dollars) : CREDIT_TIERS[0].dollars
const maxPrice = isAnnual ? annualPrice(CREDIT_TIERS[1].dollars) : CREDIT_TIERS[1].dollars
const priceSubtext = isAnnual
? 'per user/month, billed annually'
: 'per user/month, billed monthly'
const discountLabel = isAnnual ? `${discountPct}% off` : undefined
const proCta = resolveCta('pro')
const maxCta = resolveCta('max')
const enterpriseCta = resolveCta('enterprise')
return (
<>
<div className='flex flex-col items-center gap-4'>
{heading}
<BillingPeriodToggle isAnnual={isAnnual} onChange={setIsAnnual} />
</div>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4'>
<PricingCard
name='Free'
price='$0'
priceSubtext='Free forever'
cta={FREE_CTA}
sections={SECTIONS_BY_COLUMN[0]}
/>
<PricingCard
name='Pro'
price={`$${proPrice}`}
discountLabel={discountLabel}
priceSubtext={priceSubtext}
cta={proCta}
sections={SECTIONS_BY_COLUMN[1]}
/>
<PricingCard
name='Max'
price={`$${maxPrice}`}
discountLabel={discountLabel}
priceSubtext={priceSubtext}
cta={maxCta}
sections={SECTIONS_BY_COLUMN[2]}
/>
<PricingCard
name='Enterprise'
price='Custom'
priceSubtext='Tailored to your team'
cta={enterpriseCta}
sections={SECTIONS_BY_COLUMN[3]}
/>
</div>
</>
)
}
@@ -0,0 +1 @@
export { PricingStructuredData } from './pricing-structured-data'
@@ -0,0 +1,95 @@
import { CREDIT_TIERS } from '@/lib/billing/constants'
import { SITE_URL } from '@/lib/core/utils/urls'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { COMPARISON_SECTIONS } from '@/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data'
const PAGE_URL = `${SITE_URL}/pricing`
/** Feature list derived from the comparison data the cards render, so it can't drift. */
const FEATURE_LIST = Array.from(
new Set(COMPARISON_SECTIONS.flatMap((section) => section.rows.map((row) => row.label)))
)
const PRICING_JSON_LD = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebPage',
'@id': `${PAGE_URL}#webpage`,
url: PAGE_URL,
name: 'Pricing | Sim, the AI Workspace',
description:
'Pricing for Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Compare the Free, Pro, Max, and Enterprise plans.',
isPartOf: { '@id': `${SITE_URL}#website` },
about: { '@id': `${PAGE_URL}#application` },
breadcrumb: { '@id': `${PAGE_URL}#breadcrumb` },
inLanguage: 'en-US',
},
{
'@type': 'BreadcrumbList',
'@id': `${PAGE_URL}#breadcrumb`,
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
{ '@type': 'ListItem', position: 2, name: 'Pricing', item: PAGE_URL },
],
},
{
'@type': 'WebApplication',
'@id': `${PAGE_URL}#application`,
name: 'Sim',
description:
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents, connecting 1,000+ integrations and every major LLM.',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
url: SITE_URL,
featureList: FEATURE_LIST,
offers: [
{
'@type': 'Offer',
name: 'Free',
price: '0',
priceCurrency: 'USD',
description: 'Start building AI agents for free.',
},
{
'@type': 'Offer',
name: 'Pro',
price: String(CREDIT_TIERS[0].dollars),
priceCurrency: 'USD',
description: 'For growing teams. Billed per user/month.',
},
{
'@type': 'Offer',
name: 'Max',
price: String(CREDIT_TIERS[1].dollars),
priceCurrency: 'USD',
description: 'For scaling businesses. Billed per user/month.',
},
{
'@type': 'Offer',
name: 'Enterprise',
priceCurrency: 'USD',
description: 'Custom limits and infrastructure for large organizations.',
},
],
},
],
}
/**
* JSON-LD for the public pricing page - a `WebPage` (about a `WebApplication`),
* a `BreadcrumbList`, and the `WebApplication` (Sim) carrying one `Offer` per
* plan tier. Rendered server-side before any visible content so crawlers and AI
* answer engines read the structured pricing first. Mirrors the platform/
* solutions structured-data shape so the landing family stays consistent.
*
* Everything is derived from the same shared sources that drive the visible cards
* - Pro/Max monthly prices from {@link CREDIT_TIERS} and the `featureList` from
* the shared `COMPARISON_SECTIONS` - so the structured data can never drift from
* the page. Free is `$0`; Enterprise is custom and intentionally ships no price.
*
* Server Component; no client cost.
*/
export function PricingStructuredData() {
return <JsonLd data={PRICING_JSON_LD} />
}
+38
View File
@@ -0,0 +1,38 @@
import type { Metadata } from 'next'
import type { SearchParams } from 'nuqs/server'
import { buildLandingMetadata, withFilteredNoindex } from '@/lib/landing/seo'
import Pricing from '@/app/(landing)/pricing/pricing'
import { pricingSearchParamsCache } from '@/app/(landing)/pricing/search-params'
const TITLE = 'Pricing | Sim, the AI Workspace'
const DESCRIPTION =
'Pricing for Sim, the open-source AI workspace for building, deploying, and managing AI agents. Compare the Free, Pro, Max, and Enterprise plans. Start free.'
/**
* `billing` renders genuinely different prices server-side (see
* search-params.ts), so the non-default variant is noindexed rather than
* self-canonicalized — same policy as the integrations/models/blog catalogs.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<Metadata> {
const { billing } = await pricingSearchParamsCache.parse(searchParams)
const isFiltered = billing !== 'monthly'
const base = buildLandingMetadata({
title: TITLE,
description: DESCRIPTION,
path: '/pricing',
keywords:
'Sim pricing, AI workspace pricing, AI agent platform pricing, build AI agents, Pro plan, Max plan, Enterprise plan, open-source AI agents, LLM pricing',
})
return withFilteredNoindex(base, isFiltered)
}
export default async function Page({ searchParams }: { searchParams: Promise<SearchParams> }) {
await pricingSearchParamsCache.parse(searchParams)
return <Pricing />
}
@@ -0,0 +1,60 @@
import { PricingPlans } from '@/app/(landing)/pricing/components/pricing-plans'
import { PricingStructuredData } from '@/app/(landing)/pricing/components/pricing-structured-data'
/**
* sr-only product summary - an atomic citation target for AI answer engines that
* names Sim, the AI workspace, AI agents, and every plan tier.
*/
const GEO_SUMMARY =
'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Pricing scales across four plans: Free to start, Pro for growing teams, Max for scaling businesses, and Enterprise for large organizations, each connecting 1,000+ integrations and every major LLM.'
/** Server-rendered heading slot handed to the {@link PricingPlans} client island. */
const PRICING_HEADING = (
<>
<h1
id='pricing-heading'
className='text-balance text-center text-[30px] text-[var(--text-primary)]'
>
Plans that scale with you
</h1>
<p className='sr-only'>{GEO_SUMMARY}</p>
</>
)
/**
* Public `/pricing` page - the same four plans as the in-app Upgrade page,
* rendered flat on the landing background for logged-out visitors (no workspace
* chrome: no back bar, no scroll-port panel, no surrounding container). Each plan
* is a self-contained card with the full comparison breakdown transposed into it
* - there is no separate comparison table and no show/hide toggle, so the four
* cards read as one spec sheet.
*
* Server-first by design. This component owns the page frame: the shared
* route-group layout provides the chrome (light tokens, navbar, footer); this
* page owns the `<main>` region, the `#pricing` section landmark, the shared
* landing gutter, the JSON-LD
* {@link PricingStructuredData}, and the `<h1>` + sr-only GEO summary. The lone
* `<h1>` and that summary are authored here, next to the page metadata, and
* handed into the {@link PricingPlans} island as a server-rendered `heading`
* slot - so the crawlable copy stays out of the client bundle while sitting in
* the same centered cluster as the billing toggle.
*
* The only client cost is {@link PricingPlans}, which owns the single `isAnnual`
* state shared by the toggle and every card's price. Prices, credits, features,
* and CTA labels derive from the shared upgrade plan constants, so the plans stay
* aligned with the in-app Upgrade page.
*/
export default function Pricing() {
return (
<main id='main-content'>
<PricingStructuredData />
<section
id='pricing'
aria-labelledby='pricing-heading'
className='mx-auto flex w-full max-w-[1460px] flex-col gap-7 px-20 pt-8 max-sm:px-5 max-lg:px-8'
>
<PricingPlans heading={PRICING_HEADING} />
</section>
</main>
)
}
@@ -0,0 +1,28 @@
import { createSearchParamsCache, parseAsStringLiteral } from 'nuqs/server'
/** Billing periods offered on the pricing page. */
export const BILLING_PERIODS = ['monthly', 'annual'] as const
/**
* Co-located, typed URL query param for the pricing page's billing-period
* toggle. Shared by the client cards (`useQueryStates`) and the server page
* (`pricingSearchParamsCache`) so a shared `?billing=annual` URL is
* server-rendered with the annual prices already applied (no toggle flash).
*/
export const pricingParsers = {
billing: parseAsStringLiteral(BILLING_PERIODS).withDefault('monthly'),
}
/** Clean URLs, no back-stack churn — the toggle is a passive view switch. */
export const pricingUrlKeys = {
history: 'replace',
clearOnDefault: true,
} as const
/**
* Parsing this in the page's server component opts the route into dynamic
* rendering, which populates `useSearchParams` during the server render — so the
* client cards' `useQueryStates` resolves the billing period server-side and the
* initial HTML ships the correct prices (no toggle flash on a shared link).
*/
export const pricingSearchParamsCache = createSearchParamsCache(pricingParsers)