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
233 lines
9.8 KiB
TypeScript
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)))
|
|
}
|