Files
simstudioai--sim/apps/sim/app/(landing)/comparison/utils.ts
T
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

233 lines
9.8 KiB
TypeScript

import {
type CompetitorProfile,
claudeCoworkProfile,
crewaiProfile,
dustProfile,
flowiseProfile,
gumloopProfile,
langchainProfile,
langflowProfile,
makeProfile,
microsoftCopilotProfile,
n8nProfile,
openaiAgentkitProfile,
openClawProfile,
pipedreamProfile,
powerAutomateProfile,
retoolProfile,
simProfile,
stackaiProfile,
tinesProfile,
vellumProfile,
workatoProfile,
zapierProfile,
} from '@/lib/compare/data'
export interface ComparisonFaq {
question: string
answer: string
}
/** Every competitor Sim is compared against, in display/build order. */
export const ALL_COMPETITORS: CompetitorProfile[] = [
n8nProfile,
zapierProfile,
makeProfile,
gumloopProfile,
workatoProfile,
retoolProfile,
pipedreamProfile,
openaiAgentkitProfile,
tinesProfile,
stackaiProfile,
powerAutomateProfile,
vellumProfile,
claudeCoworkProfile,
langflowProfile,
flowiseProfile,
microsoftCopilotProfile,
openClawProfile,
dustProfile,
crewaiProfile,
langchainProfile,
]
const COMPETITOR_BY_SLUG = new Map(ALL_COMPETITORS.map((c) => [c.id, c]))
export function getCompetitorBySlug(slug: string): CompetitorProfile | null {
return COMPETITOR_BY_SLUG.get(slug) ?? null
}
/**
* The most recent `asOf` date across every fact source in a profile. Used as
* the sitemap `lastModified` for that competitor's comparison page, so the
* sitemap reflects when the underlying facts were actually last verified.
*/
export function getLatestVerifiedDate(profile: CompetitorProfile): Date {
let latest = 0
for (const group of Object.values(profile.facts)) {
for (const fact of Object.values(group as Record<string, { sources: { asOf: string }[] }>)) {
for (const source of fact.sources) {
const time = new Date(source.asOf).getTime()
if (!Number.isNaN(time) && time > latest) {
latest = time
}
}
}
}
return latest > 0 ? new Date(latest) : new Date()
}
/** Sim's own latest-verified date, identical across every competitor page, computed once. */
export const SIM_LATEST_VERIFIED = getLatestVerifiedDate(simProfile)
/**
* A short, atomic "who should pick which platform" verdict. The single
* block most comparison-page readers (and AI answer engines asked "should I
* use Sim or {competitor}") are actually looking for. Both sentences name
* both products explicitly and stay factual, drawing on the competitor's own
* documented standout feature rather than a generic claim.
*/
export interface ComparisonVerdict {
chooseSim: string
chooseCompetitor: string
}
export function buildBottomLine(competitor: CompetitorProfile): ComparisonVerdict {
const strength = competitor.standoutFeatures[0]
const chooseCompetitor = strength
? `Choose ${competitor.name} if you specifically need ${lowercaseFirst(strength.title)}: ${strength.description}`
: `Choose ${competitor.name} if its specific strengths, documented above, matter more to your team than an AI-native, self-hostable workspace.`
return {
chooseSim: `Choose Sim if you want an open-source, self-hostable AI workspace that treats AI agents as first-class citizens: native multi-LLM support, real-time multiplayer editing, environment promotion (dev/qa/prod), human-in-the-loop approvals, and enterprise governance (SSO, credential-level permissions, audit logs) built in rather than bolted on.`,
chooseCompetitor,
}
}
/**
* Builds the FAQ set for a "Sim vs {Competitor}" page. Answer-first, each
* question/answer pair is independently quotable per the landing GEO rules,
* and every answer names "Sim" and the competitor explicitly. Every answer
* draws on a real, sourced {@link Fact} field rather than a generic claim,
* and no two questions repeat the same answer.
*/
export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFaq[] {
const name = competitor.name
const facts = competitor.facts
const faqs: ComparisonFaq[] = [
{
question: `Is Sim a good alternative to ${name}?`,
answer: `Sim is an open-source AI workspace where teams build, deploy, and manage AI agents visually, conversationally, or with code. ${ensurePeriod(competitor.oneLiner)} Teams considering a switch typically weigh licensing (Sim is Apache 2.0 and self-hostable), pricing model, and how AI-native the platform's agent-building experience is.`,
},
{
question: `What is the main difference between Sim and ${name}?`,
answer: buildKeyDifferenceAnswer(competitor),
},
{
question: `Does Sim support self-hosting compared to ${name}?`,
answer: `Sim can be self-hosted via Docker or Kubernetes under an Apache 2.0 license, in addition to a managed cloud-hosted plan. ${name}'s self-hosting position: ${ensurePeriod(firstSentence(facts.platform.selfHostOption.value))}`,
},
{
question: `How does Sim's pricing compare to ${name}?`,
answer: `Sim uses ${summarizeFact(simProfile.facts.pricing.pricingModel.value)} ${name} uses ${summarizeFact(facts.pricing.pricingModel.value)}`,
},
{
question: `Is Sim more secure than ${name}?`,
answer: `Security is a like-for-like comparison, not a one-line verdict. Sim: ${summarizeFact(simProfile.facts.security.additionalCompliance.value)} ${name}: ${summarizeFact(facts.security.additionalCompliance.value)} Check the Security & compliance rows above for the full breakdown, including SSO, audit logging, and data residency.`,
},
{
question: `Which has stronger AI agent capabilities, Sim or ${name}?`,
answer: `Sim: ${summarizeFact(simProfile.facts.aiCapabilities.multiLlmSupport.value)} ${name}: ${summarizeFact(facts.aiCapabilities.multiLlmSupport.value)} Sim also ships native human-in-the-loop approvals, a hybrid vector-plus-keyword knowledge base, and an in-editor AI Copilot that can read execution logs and directly edit the workflow to fix a failed run.`,
},
{
question: `What are ${name}'s documented limitations compared to Sim?`,
answer: buildLimitationAnswer(competitor),
},
{
question: `Can I migrate from ${name} to Sim?`,
answer: `There is no automated one-click migration tool between ${name} and Sim. Workflows and automations need to be rebuilt in Sim's visual builder, natural-language Chat surface, or API. Most teams start by recreating their highest-value automation first to validate the switch before migrating the rest.`,
},
]
if (competitor.isWorkflowBuilder === false) {
faqs.push({
question: `Is ${name} a workflow builder like Sim?`,
answer: `Not in the same sense. ${competitor.oneLiner} Sim, by contrast, is a visual and code-based workflow builder that deploys agents as REST APIs, scheduled jobs, or chat interfaces, so the two solve different parts of the AI agent problem rather than competing feature-for-feature.`,
})
}
return faqs
}
function buildKeyDifferenceAnswer(competitor: CompetitorProfile): string {
const topFeature = competitor.standoutFeatures[0]
const topLimitation = competitor.limitations[0]
const parts = [
`Sim is built specifically as an AI agent workspace, with native multi-LLM support, an in-editor AI Copilot, and a knowledge base with hybrid vector + keyword search.`,
]
if (topFeature) {
parts.push(`${competitor.name}'s standout capability is ${formatClaim(topFeature)}`)
}
if (topLimitation) {
parts.push(`One documented limitation of ${competitor.name} is ${formatClaim(topLimitation)}`)
}
return parts.join(' ')
}
/**
* A dedicated "what's wrong with the competitor" answer, distinct from
* {@link buildKeyDifferenceAnswer} (which leads with Sim's own strengths).
* Walks every documented limitation rather than just the first, so the
* answer stays substantive even for a two-limitation profile.
*/
function buildLimitationAnswer(competitor: CompetitorProfile): string {
if (competitor.limitations.length === 0) {
return `No specific limitations of ${competitor.name} are documented in this comparison yet. See the feature-by-feature table above for a full side-by-side of every category.`
}
const parts = competitor.limitations.map(formatClaim)
return `Documented limitations of ${competitor.name} include ${parts.join('; ')}`
}
/** Renders a titled claim (a standout feature or limitation) as "lowercased title: description". */
function formatClaim(item: { title: string; description: string }): string {
return `${lowercaseFirst(item.title)}: ${item.description}`
}
function firstSentence(value: string): string {
const match = value.match(/^[^.]+\./)
return match ? match[0] : value
}
/** Appends a period if `value` doesn't already end in sentence-closing punctuation. */
export function ensurePeriod(value: string): string {
return /[.!?]$/.test(value) ? value : `${value}.`
}
/**
* Lowercases the first letter of `value`, unless its leading word is an acronym
* (e.g. "AI", "SSO", "MCP") or a CamelCase brand name (e.g. "LangChain",
* "OpenClaw", "CrewAI") - detected by 2+ uppercase letters anywhere in that
* word, not just consecutive at the start, since lowercasing either would
* mangle a proper noun ("langChain", "openClaw").
*/
export function lowercaseFirst(value: string): string {
if (value.length === 0) return value
const leadingWord = value.match(/^[A-Za-z]+/)?.[0] ?? ''
const upperCaseCount = (leadingWord.match(/[A-Z]/g) ?? []).length
if (upperCaseCount >= 2) return value
return value.charAt(0).toLowerCase() + value.slice(1)
}
/**
* Composes {@link firstSentence} + {@link lowercaseFirst} + {@link ensurePeriod} for
* stitching a fact value mid-sentence. Strips a leading "Yes:"/"No:" (or the
* comma-separated "Yes,"/"No,") token first so boolean facts don't produce
* mid-sentence "yes: ..."/"no: ..." fragments.
*/
function summarizeFact(value: string): string {
const stripped = value.replace(/^(Yes|No)(?![a-zA-Z])(?:[:,]\s*)?/, '').trim()
const base = stripped.length > 0 ? stripped : value
return ensurePeriod(lowercaseFirst(firstSentence(base)))
}