Skip to content

Commit 3aa79cf

Browse files
committed
fix(comparisons): refresh verified claims and citations
1 parent 071c29b commit 3aa79cf

30 files changed

Lines changed: 14042 additions & 13381 deletions

apps/sim/app/(landing)/comparisons/[provider]/page.tsx

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ALL_COMPETITORS,
1313
buildBottomLine,
1414
buildComparisonFaqs,
15+
getComparisonReviewDate,
1516
getCompetitorBySlug,
1617
getLatestVerifiedDate,
1718
SIM_LATEST_VERIFIED,
@@ -34,11 +35,22 @@ export async function generateStaticParams() {
3435
function factsToProperties(profile: CompetitorProfile) {
3536
return COMPARISON_SECTIONS.flatMap((section) => {
3637
const group = getFactGroup(profile, section.group)
37-
return section.rows.map((row) => ({
38-
'@type': 'PropertyValue',
39-
name: row.label,
40-
value: group[row.key]?.value ?? 'Unknown',
41-
}))
38+
return section.rows.map((row) => {
39+
const fact = group[row.key]
40+
const qualification =
41+
fact?.confidence === 'estimated'
42+
? 'Estimate: '
43+
: fact?.confidence === 'unknown'
44+
? 'Unverified: '
45+
: ''
46+
return {
47+
'@type': 'PropertyValue',
48+
name: row.label,
49+
value: `${qualification}${fact?.value ?? 'Unknown'}`,
50+
description: fact?.detail,
51+
url: fact?.sources[0]?.url,
52+
}
53+
})
4254
})
4355
}
4456

@@ -104,6 +116,7 @@ export default async function ComparisonProviderPage({
104116
const latestVerified = new Date(
105117
Math.max(SIM_LATEST_VERIFIED.getTime(), getLatestVerifiedDate(competitor).getTime())
106118
)
119+
const reviewDate = getComparisonReviewDate([simProfile, competitor])
107120

108121
const productComparisonJsonLd = {
109122
'@context': 'https://schema.org',
@@ -177,16 +190,23 @@ export default async function ComparisonProviderPage({
177190
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents
178191
visually, conversationally, or with code. Here is how Sim compares to{' '}
179192
{competitor.name} on platform architecture, AI capabilities, integrations, pricing,
180-
security, and support. Every fact below is sourced and dated, last verified{' '}
181-
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
182-
{latestVerified.toLocaleDateString('en-US', {
183-
month: 'long',
184-
day: 'numeric',
185-
year: 'numeric',
186-
timeZone: 'UTC',
187-
})}
188-
</time>
189-
.
193+
security, and support.{' '}
194+
{reviewDate ? (
195+
<>
196+
Verified against the cited sources as of{' '}
197+
<time dateTime={reviewDate.toISOString().slice(0, 10)}>
198+
{reviewDate.toLocaleDateString('en-US', {
199+
month: 'long',
200+
day: 'numeric',
201+
year: 'numeric',
202+
timeZone: 'UTC',
203+
})}
204+
</time>
205+
.{' '}
206+
</>
207+
) : null}
208+
Estimates and unverified capabilities are labeled. Plan and deployment restrictions
209+
apply as described in the sources.
190210
</p>
191211
<p className='sr-only'>
192212
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import type { ReactNode } from 'react'
5+
import { renderToStaticMarkup } from 'react-dom/server'
6+
import { describe, expect, it, vi } from 'vitest'
7+
import type { Fact } from '@/lib/compare/data'
8+
9+
vi.mock('@sim/emcn', () => ({
10+
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
11+
Tooltip: {
12+
Root: ({ children }: { children: ReactNode }) => <>{children}</>,
13+
Trigger: ({ children }: { children: ReactNode }) => <>{children}</>,
14+
Content: ({ children }: { children: ReactNode }) => (
15+
<span data-testid='source-tooltip'>{children}</span>
16+
),
17+
},
18+
}))
19+
20+
vi.mock('@sim/emcn/icons', () => ({
21+
Check: () => <svg data-icon='check' />,
22+
X: () => <svg data-icon='x' />,
23+
}))
24+
25+
import { FactValue } from '@/app/(landing)/comparisons/components/fact-value/fact-value'
26+
27+
const PRIMARY_SOURCE = {
28+
url: 'https://primary.example/compliance',
29+
label: 'Primary compliance source',
30+
asOf: '2026-09-04',
31+
}
32+
33+
function createFact(overrides: Partial<Fact> = {}): Fact {
34+
return {
35+
value: 'Complete compliance statement',
36+
detail: 'Supporting qualification',
37+
shortValue: 'Compact compliance summary',
38+
confidence: 'verified',
39+
sources: [
40+
PRIMARY_SOURCE,
41+
{
42+
url: 'https://secondary.example/compliance',
43+
label: 'Secondary compliance source',
44+
asOf: '2026-09-04',
45+
},
46+
],
47+
...overrides,
48+
}
49+
}
50+
51+
function withoutScreenReaderText(markup: string): string {
52+
return markup.replace(/<span class="sr-only">.*?<\/span>/, '')
53+
}
54+
55+
function screenReaderText(markup: string): string {
56+
const match = markup.match(/<span class="sr-only">(.*?)<\/span>/)
57+
expect(match).not.toBeNull()
58+
return match?.[1] ?? ''
59+
}
60+
61+
describe('FactValue', () => {
62+
it('shows shortValue while exposing the complete value and detail once to screen readers', () => {
63+
const markup = renderToStaticMarkup(<FactValue fact={createFact()} />)
64+
const visibleMarkup = withoutScreenReaderText(markup)
65+
const accessibleText = screenReaderText(markup)
66+
67+
expect(visibleMarkup).toContain('Compact compliance summary')
68+
expect(visibleMarkup).not.toContain('Complete compliance statement')
69+
expect(accessibleText).toBe('Complete compliance statement. Supporting qualification')
70+
expect(accessibleText.match(/Complete compliance statement/g)).toHaveLength(1)
71+
expect(accessibleText.match(/Supporting qualification/g)).toHaveLength(1)
72+
})
73+
74+
it('falls back to value and renders a fact without detail', () => {
75+
const markup = renderToStaticMarkup(
76+
<FactValue
77+
fact={createFact({
78+
value: 'Fallback visible value',
79+
shortValue: undefined,
80+
detail: undefined,
81+
sources: [],
82+
})}
83+
/>
84+
)
85+
86+
expect(withoutScreenReaderText(markup)).toContain('Fallback visible value')
87+
expect(screenReaderText(markup)).toBe('Fallback visible value')
88+
})
89+
90+
it.each([
91+
['Statement.', 'Statement. Detail'],
92+
['Statement!', 'Statement! Detail'],
93+
['Statement?', 'Statement? Detail'],
94+
['Statement.)', 'Statement.) Detail'],
95+
['Statement.”', 'Statement.” Detail'],
96+
['Statement', 'Statement. Detail'],
97+
])('joins %j and detail without duplicate punctuation', (value, expected) => {
98+
const markup = renderToStaticMarkup(
99+
<FactValue fact={createFact({ value, detail: 'Detail', sources: [] })} />
100+
)
101+
102+
expect(screenReaderText(markup)).toBe(expected)
103+
})
104+
105+
it('uses the URL and label from sources[0] for the visible source link', () => {
106+
const markup = renderToStaticMarkup(<FactValue fact={createFact()} />)
107+
const visibleMarkup = withoutScreenReaderText(markup)
108+
109+
expect(visibleMarkup).toContain(`href="${PRIMARY_SOURCE.url}"`)
110+
expect(visibleMarkup).toContain(`aria-label="${PRIMARY_SOURCE.label} (opens source)"`)
111+
expect(visibleMarkup).not.toContain('https://secondary.example/compliance')
112+
expect(visibleMarkup).not.toContain('Secondary compliance source')
113+
expect(visibleMarkup).toContain('Checked 2026-09-04')
114+
})
115+
116+
it.each(['unknown', 'estimated'] as const)(
117+
'preserves negation without a definitive icon for %s claims',
118+
(confidence) => {
119+
const markup = renderToStaticMarkup(
120+
<FactValue
121+
fact={createFact({
122+
value: 'No: self-hosted deployment',
123+
shortValue: undefined,
124+
confidence,
125+
})}
126+
/>
127+
)
128+
expect(markup).not.toContain('data-icon=')
129+
expect(withoutScreenReaderText(markup)).toContain('No: self-hosted deployment')
130+
expect(markup).toContain(confidence === 'unknown' ? '(unverified)' : '(estimate)')
131+
}
132+
)
133+
})

apps/sim/app/(landing)/comparisons/components/fact-value/fact-value.tsx

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,20 @@ export interface FactValueProps {
77
fact: Fact
88
}
99

10+
const TERMINAL_PUNCTUATION = /[.!?][\])}'"]*$/
11+
1012
/**
11-
* Renders one {@link Fact} for a glancing reader while keeping the full
12-
* granular fact server-rendered for crawlers and AI answer engines.
13-
*
14-
* - A true "Yes"/"No" fact renders as an icon alone (a monochrome check or
15-
* muted cross, no colored pass/fail styling), no visible text, since the
16-
* label column and surrounding context already say what's being asked.
17-
* - Any other fact shows its `shortValue` (a compact, pre-authored
18-
* restatement of `value`), never the full sentence.
19-
* - `Tooltip` here is a cursor-following mini-bubble meant for a short
20-
* one-line label (see its own docs/usages: "Refresh", "last updated: X")
21-
* . It is deliberately NOT used to hold paragraph-length detail text, only
22-
* the compact source citation, which is exactly what it's designed for.
23-
* - When a source exists, the visible glance (icon or `shortValue` text)
24-
* IS the hover/click target for that source, via `SourceLink`, rather
25-
* than a separate info-icon next to every value. One affordance per
26-
* fact keeps a 58-row table from reading as icon-cluttered.
27-
* - A `sr-only` span always carries the complete value, detail, and source
28-
* in the initial server-rendered HTML, independent of hover/JS state, so
29-
* an LLM or crawler reading the page gets full granularity even though a
30-
* human sees only the compact glance.
13+
* Keeps the full value and detail in server-rendered text while displaying
14+
* a compact value. Only verified boolean claims use icons; other claims keep
15+
* their confidence labels and fall back to the original value when needed.
16+
* Source tooltips stay brief so qualifications remain available without hover.
3117
*/
3218
export function FactValue({ fact }: FactValueProps) {
33-
const { status, text } = parseFactValue(fact.value)
34-
const isBoolean = status === 'yes' || status === 'no'
19+
const { status } = parseFactValue(fact.value)
20+
const isBoolean = fact.confidence === 'verified' && (status === 'yes' || status === 'no')
3521
const primarySource = fact.sources[0]
3622

37-
const detailSeparator = /[.!?]$/.test(fact.value.trimEnd()) ? ' ' : '. '
23+
const detailSeparator = TERMINAL_PUNCTUATION.test(fact.value.trimEnd()) ? ' ' : '. '
3824
const fullText = fact.detail ? `${fact.value}${detailSeparator}${fact.detail}` : fact.value
3925

4026
const glance = isBoolean ? (
@@ -45,9 +31,7 @@ export function FactValue({ fact }: FactValueProps) {
4531
)
4632
) : null
4733

48-
// A pure yes/no fact renders as an icon only. The "why" lives in the
49-
// source link and the sr-only text, not cluttering the glance view.
50-
const shortText = isBoolean ? null : (fact.shortValue ?? text)
34+
const shortText = isBoolean ? null : (fact.shortValue ?? fact.value)
5135

5236
const valueNode = glance ?? (
5337
<span className='truncate text-[var(--text-body)] text-small'>{shortText}</span>
@@ -62,6 +46,11 @@ export function FactValue({ fact }: FactValueProps) {
6246
) : (
6347
valueNode
6448
)}
49+
{fact.confidence !== 'verified' ? (
50+
<span className='shrink-0 text-[var(--text-muted)] text-caption'>
51+
{fact.confidence === 'estimated' ? '(estimate)' : '(unverified)'}
52+
</span>
53+
) : null}
6554
<span className='sr-only'>{fullText}</span>
6655
</div>
6756
)

apps/sim/app/(landing)/comparisons/components/source-info/source-info.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,8 @@ export interface SourceLinkProps {
1212
}
1313

1414
/**
15-
* Wraps a fact's visible value (or a card's title) so hovering it directly
16-
* shows a one-line "Source: X" tooltip, and clicking it opens the source,
17-
* rather than a separate info-icon affordance next to every value. One
18-
* hover/click target per fact instead of two keeps the dense comparison
19-
* table and card lists from reading as icon-cluttered. Every {@link FactSource}
20-
* carries a real, publicly reachable URL (enforced by the type), so this
21-
* always renders as a link.
15+
* Uses the visible value or title as the citation target to keep dense tables
16+
* and cards compact. The tooltip includes the source label and review date.
2217
*/
2318
export function SourceLink({ source, children, className }: SourceLinkProps) {
2419
return (
@@ -34,7 +29,9 @@ export function SourceLink({ source, children, className }: SourceLinkProps) {
3429
{children}
3530
</a>
3631
</Tooltip.Trigger>
37-
<Tooltip.Content>Source: {source.label}</Tooltip.Content>
32+
<Tooltip.Content>
33+
Source: {source.label} · Checked {source.asOf}
34+
</Tooltip.Content>
3835
</Tooltip.Root>
3936
)
4037
}

apps/sim/app/(landing)/comparisons/page.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { simProfile } from '@/lib/compare/data'
44
import { SITE_URL } from '@/lib/core/utils/urls'
55
import { buildLandingMetadata } from '@/lib/landing/seo'
66
import { BrandIconTile } from '@/app/(landing)/comparisons/components/brand-icon-tile'
7-
import { ALL_COMPETITORS, ensurePeriod, lowercaseFirst } from '@/app/(landing)/comparisons/utils'
7+
import {
8+
ALL_COMPETITORS,
9+
ensurePeriod,
10+
getComparisonReviewDate,
11+
lowercaseFirst,
12+
} from '@/app/(landing)/comparisons/utils'
813
import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow'
914
import { JsonLd } from '@/app/(landing)/components/json-ld'
1015
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
@@ -17,12 +22,12 @@ const faqItems = [
1722
{
1823
question: 'How does Sim compare to workflow automation and AI agent platforms?',
1924
answer:
20-
'Sim is an open-source AI workspace where teams build, deploy, and manage AI agents visually, conversationally, or with code. Compared to workflow automation tools like n8n, Zapier, and Make, Sim treats AI agents as first-class building blocks rather than an add-on to data routing, and ships a native knowledge base, MCP support, and an in-editor AI Copilot. Compared to enterprise AI builders like Gumloop, Workato, StackAI, and Vellum, Sim is fully open source (Apache 2.0) and self-hostable, so teams can run it on their own infrastructure.',
25+
'Sim combines a visual workflow canvas, natural-language assistance, multiple model providers, a knowledge base, and MCP support. Its core can run on your own infrastructure. Each comparison examines the specific product surfaces, plans, and deployment options offered by Sim and the other platform.',
2126
},
2227
{
2328
question: 'Is Sim open source?',
2429
answer:
25-
'Yes. Sim is released under the Apache License 2.0 and can be self-hosted via Docker or Kubernetes, or used as a managed cloud-hosted service.',
30+
'Sim’s core is Apache-2.0 licensed and can be self-hosted with Docker or Kubernetes. Enterprise features have separate license terms, and some capabilities, including Chat, use external services.',
2631
},
2732
{
2833
question: 'Which AI agent platform should I choose?',
@@ -31,15 +36,15 @@ const faqItems = [
3136
},
3237
{
3338
question: 'Is Sim free to use?',
34-
answer: `${ensurePeriod(simProfile.facts.pricing.freeTier.value)} Sim is also free to self-host under the Apache 2.0 license with no seat or usage limits beyond your own infrastructure.`,
39+
answer: `${ensurePeriod(simProfile.facts.pricing.freeTier.value)} You can also self-host the Apache-2.0 core. Infrastructure, model providers, external services, and Enterprise licensing may carry separate costs.`,
3540
},
3641
{
3742
question: 'Does Sim support MCP (Model Context Protocol)?',
38-
answer: `${ensurePeriod(simProfile.facts.aiCapabilities.mcpSupport.value)} Sim can also publish any deployed workflow as its own MCP server, so it works as both an MCP client and an MCP server.`,
43+
answer: `${ensurePeriod(simProfile.facts.aiCapabilities.mcpSupport.value)} ${ensurePeriod(simProfile.facts.integrations.mcpPublishing.value)}`,
3944
},
4045
{
4146
question: 'How many integrations does Sim support?',
42-
answer: `Sim ships ${ensurePeriod(lowercaseFirst(simProfile.facts.integrations.integrationCount.value))} Combined with native MCP client support, teams can extend Sim to any service with a public API, not just the built-in catalog.`,
47+
answer: `Sim lists ${ensurePeriod(lowercaseFirst(simProfile.facts.integrations.integrationCount.value))} MCP, API, and custom code connections provide additional extension options. Service counts and individual tool-action counts are different measures.`,
4348
},
4449
]
4550

@@ -60,6 +65,7 @@ export const metadata: Metadata = buildLandingMetadata({
6065
})
6166

6267
export default function ComparisonHubPage() {
68+
const reviewDate = getComparisonReviewDate([simProfile, ...ALL_COMPETITORS])
6369
const breadcrumbJsonLd = {
6470
'@context': 'https://schema.org',
6571
'@type': 'BreadcrumbList',
@@ -116,6 +122,22 @@ export default function ComparisonHubPage() {
116122
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.
117123
See how Sim compares to workflow automation platforms and AI agent builders on
118124
platform architecture, AI capabilities, integrations, pricing, security, and support.
125+
{reviewDate ? (
126+
<>
127+
{' '}
128+
Verified against the cited sources as of{' '}
129+
<time dateTime={reviewDate.toISOString().slice(0, 10)}>
130+
{reviewDate.toLocaleDateString('en-US', {
131+
month: 'long',
132+
day: 'numeric',
133+
year: 'numeric',
134+
timeZone: 'UTC',
135+
})}
136+
</time>
137+
.
138+
</>
139+
) : null}{' '}
140+
Estimates and unverified capabilities are labeled on each comparison.
119141
</p>
120142
<p className='sr-only'>
121143
This directory lists every Sim vs. competitor comparison page, covering workflow

0 commit comments

Comments
 (0)