Skip to content
3 changes: 3 additions & 0 deletions apps/sim/app/(landing)/comparison/comparison-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [
{ key: 'versionControlDepth', label: 'Version control' },
{ key: 'realtimeCollaboration', label: 'Realtime collaboration' },
{ key: 'nativeFileStorage', label: 'Native file storage' },
{ key: 'subWorkflows', label: 'Sub-workflows (composition)' },
],
}),
defineSection({
Expand All @@ -91,6 +92,7 @@ export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [
{ key: 'modelAndToolGovernance', label: 'Model & tool governance' },
{ key: 'credentialGovernance', label: 'Credential governance' },
{ key: 'sso', label: 'Single sign-on (SSO)' },
{ key: 'thirdPartyVetting', label: 'Vetted first-party integrations' },
{ key: 'piiRedaction', label: 'PII redaction' },
{ key: 'dataRetention', label: 'Custom data retention' },
{ key: 'whiteLabeling', label: 'White-labeling' },
Expand All @@ -114,6 +116,7 @@ export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [
{ key: 'nativeChatDeployment', label: 'Native chat deployment' },
{ key: 'parallelExecution', label: 'Parallel execution' },
{ key: 'a2aProtocol', label: 'Agent2Agent (A2A) protocol' },
{ key: 'loopIteration', label: 'Loop / iteration block' },
],
}),
defineSection({
Expand Down
12 changes: 7 additions & 5 deletions apps/sim/app/(landing)/comparison/fact-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ export interface ParsedFact {
// The negative lookahead (?![a-zA-Z]) requires the "Yes"/"No" token to end at a word
// boundary, so values like "Not documented" or "Not publicly documented" (which start
// with the letters "No" but aren't the boolean token) fall through to 'neutral' instead
// of being misread as a "No" status.
const STATUS_PREFIX = /^(Yes|No)(?![a-zA-Z])(?::\s*)?(.*)$/s
// of being misread as a "No" status. The separator group accepts either a colon
// ("Yes: ...") or a comma ("Yes, but ...") since both are used throughout the dataset.
const STATUS_PREFIX = /^(Yes|No)(?![a-zA-Z])(?:[:,]\s*)?(.*)$/s

/**
* Splits a {@link Fact.value} string into a status (for a compact icon) and
* the remaining descriptive text. Every fact in `apps/sim/lib/compare/data`
* that represents a yes/no capability is written as `"Yes: ..."` / `"No:
* ..."`. This is the single place that convention is parsed, so the
* comparison table and the key-differences strip render it identically.
* that represents a yes/no capability is written as `"Yes: ..."` / `"No: ..."`,
* or occasionally `"Yes, ..."` / `"No, ..."` as a more natural continuation.
* This is the single place that convention is parsed, so the comparison table
* and the key-differences strip render it identically.
*/
export function parseFactValue(value: string): ParsedFact {
const match = value.match(STATUS_PREFIX)
Expand Down
32 changes: 25 additions & 7 deletions apps/sim/app/(landing)/comparison/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import {
type CompetitorProfile,
claudeCoworkProfile,
crewaiProfile,
dustProfile,
flowiseProfile,
gumloopProfile,
langchainProfile,
langflowProfile,
makeProfile,
microsoftCopilotProfile,
n8nProfile,
openaiAgentkitProfile,
openClawProfile,
pipedreamProfile,
powerAutomateProfile,
retoolProfile,
Expand Down Expand Up @@ -40,6 +45,11 @@ export const ALL_COMPETITORS: CompetitorProfile[] = [
claudeCoworkProfile,
langflowProfile,
flowiseProfile,
microsoftCopilotProfile,
openClawProfile,
dustProfile,
crewaiProfile,
langchainProfile,
Comment thread
waleedlatif1 marked this conversation as resolved.
]

const COMPETITOR_BY_SLUG = new Map(ALL_COMPETITORS.map((c) => [c.id, c]))
Expand Down Expand Up @@ -108,7 +118,7 @@ export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFa
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. ${name} is ${lowercaseFirst(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.`,
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}?`,
Expand Down Expand Up @@ -194,21 +204,29 @@ export function ensurePeriod(value: string): string {
return /[.!?]$/.test(value) ? value : `${value}.`
}

/** Lowercases the first letter of `value`, unless it starts with an acronym (e.g. "AI", "SSO", "MCP"). */
/**
* 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
// Leave a leading acronym (2+ consecutive capitals, e.g. "AI", "SSO", "MCP") alone.
if (/^[A-Z]{2,}/.test(value)) 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:" token first so
* boolean facts don't produce mid-sentence "yes: ..."/"no: ..." fragments.
* 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 stripped = value.replace(/^(Yes|No)(?![a-zA-Z])(?:[:,]\s*)?/, '').trim()
const base = stripped.length > 0 ? stripped : value
return ensurePeriod(lowercaseFirst(firstSentence(base)))
}
Loading
Loading