mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(landing): added enterprise section (#3637)
* improvement(landing): added enterprise section * make components interactive * added more things to pricing sheet * remove debug log * fix(landing): remove dead DotGrid component and fix enterprise CTA to use Link Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
168cd585cb
commit
28de28899a
@@ -4,14 +4,484 @@
|
||||
* SEO:
|
||||
* - `<section id="enterprise" aria-labelledby="enterprise-heading">`.
|
||||
* - `<h2 id="enterprise-heading">` for the section title.
|
||||
* - Compliance certs (SOC2, HIPAA) as visible `<strong>` text.
|
||||
* - Compliance certs (SOC 2, HIPAA) as visible `<strong>` text.
|
||||
* - Enterprise CTA links to contact form via `<a>` with `rel="noopener noreferrer"`.
|
||||
*
|
||||
* GEO:
|
||||
* - Entity-rich: "Sim is SOC2 and HIPAA compliant" — not "We are compliant."
|
||||
* - Entity-rich: "Sim is SOC 2 and HIPAA compliant" — not "We are compliant."
|
||||
* - `<ul>` checklist of features (SSO, RBAC, audit logs, SLA, on-premise deployment)
|
||||
* as an atomic answer block for "What enterprise features does Sim offer?".
|
||||
*/
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { Lock } from '@/components/emcn/icons'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
|
||||
/** Consistent color per actor — same pattern as Collaboration section cursors. */
|
||||
const ACTOR_COLORS: Record<string, string> = {
|
||||
'Sarah K.': '#2ABBF8',
|
||||
'Sid G.': '#33C482',
|
||||
'Theo L.': '#FA4EDF',
|
||||
'Abhay K.': '#FFCC02',
|
||||
'Danny S.': '#FF6B35',
|
||||
}
|
||||
|
||||
/** Left accent bar opacity by recency — newest is brightest. */
|
||||
const ACCENT_OPACITIES = [0.75, 0.45, 0.28, 0.15, 0.07] as const
|
||||
|
||||
/** Human-readable label per resource type. */
|
||||
const RESOURCE_TYPE_LABEL: Record<string, string> = {
|
||||
workflow: 'Workflow',
|
||||
member: 'Member',
|
||||
byok_key: 'BYOK Key',
|
||||
api_key: 'API Key',
|
||||
permission_group: 'Permission Group',
|
||||
credential_set: 'Credential Set',
|
||||
knowledge_base: 'Knowledge Base',
|
||||
environment: 'Environment',
|
||||
mcp_server: 'MCP Server',
|
||||
file: 'File',
|
||||
webhook: 'Webhook',
|
||||
chat: 'Chat',
|
||||
table: 'Table',
|
||||
folder: 'Folder',
|
||||
document: 'Document',
|
||||
}
|
||||
|
||||
interface LogEntry {
|
||||
id: number
|
||||
actor: string
|
||||
/** Matches the `description` field stored by recordAudit() */
|
||||
description: string
|
||||
resourceType: string
|
||||
/** Unix ms timestamp of when this entry was "received" */
|
||||
insertedAt: number
|
||||
}
|
||||
|
||||
function formatTimeAgo(insertedAt: number): string {
|
||||
const elapsed = Date.now() - insertedAt
|
||||
if (elapsed < 8_000) return 'just now'
|
||||
if (elapsed < 60_000) return `${Math.floor(elapsed / 1000)}s ago`
|
||||
return `${Math.floor(elapsed / 60_000)}m ago`
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry templates using real description strings from the actual recordAudit()
|
||||
* calls across the codebase (e.g. `Added BYOK key for openai`,
|
||||
* `Invited alex@acme.com to workspace as member`).
|
||||
*/
|
||||
const ENTRY_TEMPLATES: Omit<LogEntry, 'id' | 'insertedAt'>[] = [
|
||||
{ actor: 'Sarah K.', description: 'Deployed workflow "Email Triage"', resourceType: 'workflow' },
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Invited alex@acme.com to workspace as member',
|
||||
resourceType: 'member',
|
||||
},
|
||||
{ actor: 'Theo L.', description: 'Added BYOK key for openai', resourceType: 'byok_key' },
|
||||
{ actor: 'Sarah K.', description: 'Created workflow "Invoice Parser"', resourceType: 'workflow' },
|
||||
{
|
||||
actor: 'Abhay K.',
|
||||
description: 'Created permission group "Engineering"',
|
||||
resourceType: 'permission_group',
|
||||
},
|
||||
{ actor: 'Danny S.', description: 'Created API key "Production Key"', resourceType: 'api_key' },
|
||||
{
|
||||
actor: 'Theo L.',
|
||||
description: 'Changed permissions for sam@acme.com to editor',
|
||||
resourceType: 'member',
|
||||
},
|
||||
{ actor: 'Sarah K.', description: 'Uploaded file "Q3_Report.pdf"', resourceType: 'file' },
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Created credential set "Prod Keys"',
|
||||
resourceType: 'credential_set',
|
||||
},
|
||||
{
|
||||
actor: 'Abhay K.',
|
||||
description: 'Created knowledge base "Internal Docs"',
|
||||
resourceType: 'knowledge_base',
|
||||
},
|
||||
{ actor: 'Danny S.', description: 'Updated environment variables', resourceType: 'environment' },
|
||||
{
|
||||
actor: 'Sarah K.',
|
||||
description: 'Added tool "search_web" to MCP server',
|
||||
resourceType: 'mcp_server',
|
||||
},
|
||||
{ actor: 'Sid G.', description: 'Created webhook "Stripe Payment"', resourceType: 'webhook' },
|
||||
{ actor: 'Theo L.', description: 'Deployed chat "Support Assistant"', resourceType: 'chat' },
|
||||
{ actor: 'Abhay K.', description: 'Created table "Lead Tracker"', resourceType: 'table' },
|
||||
{ actor: 'Danny S.', description: 'Revoked API key "Staging Key"', resourceType: 'api_key' },
|
||||
{
|
||||
actor: 'Sarah K.',
|
||||
description: 'Duplicated workflow "Data Enrichment"',
|
||||
resourceType: 'workflow',
|
||||
},
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Removed member theo@acme.com from workspace',
|
||||
resourceType: 'member',
|
||||
},
|
||||
{
|
||||
actor: 'Theo L.',
|
||||
description: 'Updated knowledge base "Product Docs"',
|
||||
resourceType: 'knowledge_base',
|
||||
},
|
||||
{ actor: 'Abhay K.', description: 'Created folder "Finance Workflows"', resourceType: 'folder' },
|
||||
{
|
||||
actor: 'Danny S.',
|
||||
description: 'Uploaded document "onboarding-guide.pdf"',
|
||||
resourceType: 'document',
|
||||
},
|
||||
{
|
||||
actor: 'Sarah K.',
|
||||
description: 'Updated credential set "Prod Keys"',
|
||||
resourceType: 'credential_set',
|
||||
},
|
||||
{
|
||||
actor: 'Sid G.',
|
||||
description: 'Added member abhay@acme.com to permission group "Engineering"',
|
||||
resourceType: 'permission_group',
|
||||
},
|
||||
{ actor: 'Theo L.', description: 'Locked workflow "Customer Sync"', resourceType: 'workflow' },
|
||||
]
|
||||
|
||||
const INITIAL_OFFSETS_MS = [0, 20_000, 75_000, 240_000, 540_000]
|
||||
|
||||
const MARQUEE_KEYFRAMES = `
|
||||
@keyframes marquee {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-25%); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes marquee { 0%, 100% { transform: none; } }
|
||||
}
|
||||
`
|
||||
|
||||
const FEATURE_TAGS = [
|
||||
'Access Control',
|
||||
'Self-Hosting',
|
||||
'Bring Your Own Key',
|
||||
'Credential Sharing',
|
||||
'Custom Limits',
|
||||
'Admin API',
|
||||
'White Labeling',
|
||||
'Dedicated Support',
|
||||
'99.99% Uptime SLA',
|
||||
'Workflow Versioning',
|
||||
'On-Premise',
|
||||
'Organizations',
|
||||
'Workspace Export',
|
||||
'Audit Logs',
|
||||
] as const
|
||||
|
||||
interface AuditRowProps {
|
||||
entry: LogEntry
|
||||
index: number
|
||||
}
|
||||
|
||||
function AuditRow({ entry, index }: AuditRowProps) {
|
||||
const color = ACTOR_COLORS[entry.actor] ?? '#F6F6F6'
|
||||
const accentOpacity = ACCENT_OPACITIES[index] ?? 0.04
|
||||
const timeAgo = formatTimeAgo(entry.insertedAt)
|
||||
const resourceLabel = RESOURCE_TYPE_LABEL[entry.resourceType]
|
||||
|
||||
return (
|
||||
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#191919] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
|
||||
{/* Left accent bar — brightness encodes recency */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute top-0 bottom-0 left-0 w-[2px] transition-opacity duration-150 group-hover:opacity-100'
|
||||
style={{ backgroundColor: color, opacity: accentOpacity }}
|
||||
/>
|
||||
|
||||
{/* Row content */}
|
||||
<div className='flex min-w-0 items-center gap-3 py-[10px] pr-4 pl-5'>
|
||||
{/* Actor avatar */}
|
||||
<div
|
||||
className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full'
|
||||
style={{ backgroundColor: `${color}20` }}
|
||||
>
|
||||
<span className='font-[500] font-season text-[9px] leading-none' style={{ color }}>
|
||||
{entry.actor[0]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<span className='w-[56px] shrink-0 font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
{timeAgo}
|
||||
</span>
|
||||
|
||||
{/* Description — description hidden on mobile to avoid truncation */}
|
||||
<span className='min-w-0 truncate font-[430] font-season text-[12px] leading-none tracking-[0.02em]'>
|
||||
<span className='text-[#F6F6F6]/80'>{entry.actor}</span>
|
||||
<span className='hidden sm:inline'>
|
||||
<span className='text-[#F6F6F6]/40'> · </span>
|
||||
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Resource type label — formatted name, neutral so it doesn't compete with actor colors */}
|
||||
{resourceLabel && (
|
||||
<span className='ml-auto shrink-0 rounded border border-[#2A2A2A] px-[7px] py-[3px] font-[430] font-season text-[#F6F6F6]/25 text-[10px] leading-none tracking-[0.04em]'>
|
||||
{resourceLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AuditLogPreview() {
|
||||
const counterRef = useRef(ENTRY_TEMPLATES.length)
|
||||
const templateIndexRef = useRef(5 % ENTRY_TEMPLATES.length)
|
||||
|
||||
const now = Date.now()
|
||||
const [entries, setEntries] = useState<LogEntry[]>(() =>
|
||||
ENTRY_TEMPLATES.slice(0, 5).map((t, i) => ({
|
||||
...t,
|
||||
id: i,
|
||||
insertedAt: now - INITIAL_OFFSETS_MS[i],
|
||||
}))
|
||||
)
|
||||
const [, tick] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const addInterval = setInterval(() => {
|
||||
const template = ENTRY_TEMPLATES[templateIndexRef.current]
|
||||
templateIndexRef.current = (templateIndexRef.current + 1) % ENTRY_TEMPLATES.length
|
||||
|
||||
setEntries((prev) => [
|
||||
{ ...template, id: counterRef.current++, insertedAt: Date.now() },
|
||||
...prev.slice(0, 4),
|
||||
])
|
||||
}, 2600)
|
||||
|
||||
// Refresh time labels every 5s so "just now" ages to "Xs ago"
|
||||
const tickInterval = setInterval(() => tick((n) => n + 1), 5_000)
|
||||
|
||||
return () => {
|
||||
clearInterval(addInterval)
|
||||
clearInterval(tickInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mx-6 mt-6 overflow-hidden rounded-[8px] border border-[#2A2A2A] md:mx-8 md:mt-8'>
|
||||
{/* Header */}
|
||||
<div className='flex items-center justify-between border-[#2A2A2A] border-b bg-[#161616] px-4 py-[10px]'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{/* Pulsing live indicator */}
|
||||
<span className='relative flex h-[8px] w-[8px]'>
|
||||
<span
|
||||
className='absolute inline-flex h-full w-full animate-ping rounded-full opacity-50'
|
||||
style={{ backgroundColor: '#33C482' }}
|
||||
/>
|
||||
<span
|
||||
className='relative inline-flex h-[8px] w-[8px] rounded-full'
|
||||
style={{ backgroundColor: '#33C482' }}
|
||||
/>
|
||||
</span>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/40 text-[11px] uppercase tracking-[0.08em]'>
|
||||
Audit Log
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='rounded border border-[#2A2A2A] px-[8px] py-[3px] font-[430] font-season text-[#F6F6F6]/20 text-[11px] tracking-[0.02em]'>
|
||||
Export
|
||||
</span>
|
||||
<span className='rounded border border-[#2A2A2A] px-[8px] py-[3px] font-[430] font-season text-[#F6F6F6]/20 text-[11px] tracking-[0.02em]'>
|
||||
Filter
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log entries — new items push existing ones down */}
|
||||
<div className='overflow-hidden'>
|
||||
<AnimatePresence mode='popLayout' initial={false}>
|
||||
{entries.map((entry, index) => (
|
||||
<motion.div
|
||||
key={entry.id}
|
||||
layout
|
||||
initial={{ y: -48, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
layout: {
|
||||
type: 'spring',
|
||||
stiffness: 380,
|
||||
damping: 38,
|
||||
mass: 0.8,
|
||||
},
|
||||
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
|
||||
opacity: { duration: 0.25 },
|
||||
}}
|
||||
>
|
||||
<AuditRow entry={entry} index={index} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrustStrip() {
|
||||
return (
|
||||
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-[8px] border border-[#2A2A2A] sm:grid-cols-3 md:mx-8'>
|
||||
{/* SOC 2 + HIPAA combined */}
|
||||
<Link
|
||||
href='https://trust.delve.co/sim-studio'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
alt='SOC 2 Type II'
|
||||
width={22}
|
||||
height={22}
|
||||
className='shrink-0 object-contain'
|
||||
/>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
SOC 2 & HIPAA
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
|
||||
Type II · PHI protected →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Open Source — center */}
|
||||
<Link
|
||||
href='https://github.com/simstudioai/sim'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
>
|
||||
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#FFCC02]/10'>
|
||||
<GithubIcon width={11} height={11} className='text-[#FFCC02]/75' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
Open Source
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em] transition-colors group-hover:text-[#F6F6F6]/55'>
|
||||
View on GitHub →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* SSO */}
|
||||
<div className='flex items-center gap-3 px-4 py-[14px]'>
|
||||
<div className='flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full bg-[#2ABBF8]/10'>
|
||||
<Lock className='h-[14px] w-[14px] text-[#2ABBF8]/75' />
|
||||
</div>
|
||||
<div className='flex flex-col gap-[3px]'>
|
||||
<strong className='font-[430] font-season text-[13px] text-white leading-none'>
|
||||
SSO & SCIM
|
||||
</strong>
|
||||
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[11px] leading-none tracking-[0.02em]'>
|
||||
Okta, Azure AD, Google
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Enterprise() {
|
||||
return null
|
||||
return (
|
||||
<section id='enterprise' aria-labelledby='enterprise-heading' className='bg-[#F6F6F6]'>
|
||||
<div className='px-4 pt-[60px] pb-[40px] sm:px-8 sm:pt-[80px] sm:pb-0 md:px-[80px] md:pt-[100px]'>
|
||||
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
|
||||
<Badge
|
||||
variant='blue'
|
||||
size='md'
|
||||
dot
|
||||
className='bg-[#FFCC02]/10 font-season text-[#FFCC02] uppercase tracking-[0.02em]'
|
||||
>
|
||||
Enterprise
|
||||
</Badge>
|
||||
|
||||
<h2
|
||||
id='enterprise-heading'
|
||||
className='max-w-[600px] font-[430] font-season text-[#1C1C1C] text-[32px] leading-[100%] tracking-[-0.02em] sm:text-[36px] md:text-[40px]'
|
||||
>
|
||||
Enterprise features for
|
||||
<br />
|
||||
fast, scalable workflows
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 overflow-hidden rounded-[12px] bg-[#1C1C1C] sm:mt-10 md:mt-12'>
|
||||
<AuditLogPreview />
|
||||
<TrustStrip />
|
||||
|
||||
{/* Scrolling feature ticker */}
|
||||
<div className='relative mt-6 overflow-hidden border-[#2A2A2A] border-t'>
|
||||
<style dangerouslySetInnerHTML={{ __html: MARQUEE_KEYFRAMES }} />
|
||||
{/* Fade edges */}
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 bottom-0 left-0 z-10 w-16'
|
||||
style={{ background: 'linear-gradient(to right, #1C1C1C, transparent)' }}
|
||||
/>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute top-0 right-0 bottom-0 z-10 w-16'
|
||||
style={{ background: 'linear-gradient(to left, #1C1C1C, transparent)' }}
|
||||
/>
|
||||
{/* Duplicate tags for seamless loop */}
|
||||
<div className='flex w-max' style={{ animation: 'marquee 30s linear infinite' }}>
|
||||
{[...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS, ...FEATURE_TAGS].map(
|
||||
(tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className='whitespace-nowrap border-[#2A2A2A] border-r px-5 py-4 font-[430] font-season text-[#F6F6F6]/40 text-[13px] leading-none tracking-[0.02em]'
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between border-[#2A2A2A] border-t px-6 py-5 md:px-8 md:py-6'>
|
||||
<p className='font-[430] font-season text-[#F6F6F6]/40 text-[15px] leading-[150%] tracking-[0.02em]'>
|
||||
Ready for growth?
|
||||
</p>
|
||||
<Link
|
||||
href='/contact'
|
||||
className='group/cta inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-white bg-white px-[10px] font-[430] font-season text-[14px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
|
||||
>
|
||||
Book a demo
|
||||
<span className='relative h-[10px] w-[10px] shrink-0'>
|
||||
<ChevronDown className='-rotate-90 absolute inset-0 h-[10px] w-[10px] transition-opacity duration-150 group-hover/cta:opacity-0' />
|
||||
<svg
|
||||
className='absolute inset-0 h-[10px] w-[10px] opacity-0 transition-opacity duration-150 group-hover/cta:opacity-100'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
>
|
||||
<path
|
||||
d='M1 5H8M5.5 2L8.5 5L5.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { type SVGProps, useEffect, useRef, useState } from 'react'
|
||||
import { AnimatePresence, motion, useInView } from 'framer-motion'
|
||||
import ReactMarkdown, { type Components } from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import { Database, File, Library, Table } from '@/components/emcn/icons'
|
||||
import {
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
xAIIcon,
|
||||
} from '@/components/icons'
|
||||
import { CsvIcon, JsonIcon, MarkdownIcon, PdfIcon } from '@/components/icons/document-icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface FeaturesPreviewProps {
|
||||
activeTab: number
|
||||
@@ -124,7 +127,7 @@ const EXPAND_TARGETS: Record<number, { row: number; col: number }> = {
|
||||
}
|
||||
|
||||
const EXPAND_ROW_COUNTS: Record<number, number> = {
|
||||
1: 10,
|
||||
1: 8,
|
||||
2: 10,
|
||||
3: 10,
|
||||
4: 7,
|
||||
@@ -603,7 +606,28 @@ const MOCK_KB_DATA = [
|
||||
['metrics.csv', '1.4 MB', '5.8k', '38', 'enabled'],
|
||||
] as const
|
||||
|
||||
const MD_COMPONENTS: Components = {
|
||||
h1: ({ children }) => (
|
||||
<h1 className='mb-4 border-[#E5E5E5] border-b pb-2 font-semibold text-[#1C1C1C] text-[20px]'>
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className='mt-5 mb-3 border-[#E5E5E5] border-b pb-1.5 font-semibold text-[#1C1C1C] text-[16px]'>
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
ul: ({ children }) => <ul className='mb-3 list-disc pl-[24px]'>{children}</ul>,
|
||||
ol: ({ children }) => <ol className='mb-3 list-decimal pl-[24px]'>{children}</ol>,
|
||||
li: ({ children }) => (
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>{children}</li>
|
||||
),
|
||||
p: ({ children }) => <p className='mb-3 text-[#1C1C1C] text-[14px] leading-[1.6]'>{children}</p>,
|
||||
}
|
||||
|
||||
function MockFullFiles() {
|
||||
const [source, setSource] = useState(MOCK_MD_SOURCE)
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
@@ -622,9 +646,13 @@ function MockFullFiles() {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4, delay: 0.3 }}
|
||||
>
|
||||
<pre className='h-full overflow-auto whitespace-pre-wrap p-[24px] font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7]'>
|
||||
{MOCK_MD_SOURCE}
|
||||
</pre>
|
||||
<textarea
|
||||
value={source}
|
||||
onChange={(e) => setSource(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoCorrect='off'
|
||||
className='h-full w-full resize-none overflow-auto whitespace-pre-wrap bg-transparent p-[24px] font-[300] font-mono text-[#1C1C1C] text-[12px] leading-[1.7] outline-none'
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className='h-full w-px shrink-0 bg-[#E5E5E5]' />
|
||||
@@ -636,47 +664,9 @@ function MockFullFiles() {
|
||||
transition={{ duration: 0.4, delay: 0.5 }}
|
||||
>
|
||||
<div className='h-full overflow-auto p-[24px]'>
|
||||
<h1 className='mb-4 border-[#E5E5E5] border-b pb-2 font-semibold text-[#1C1C1C] text-[20px]'>
|
||||
Meeting Notes
|
||||
</h1>
|
||||
<h2 className='mt-5 mb-3 border-[#E5E5E5] border-b pb-1.5 font-semibold text-[#1C1C1C] text-[16px]'>
|
||||
Action Items
|
||||
</h2>
|
||||
<ul className='mb-3 list-disc pl-[24px]'>
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Review Q1 metrics with Sarah
|
||||
</li>
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Update API documentation
|
||||
</li>
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Schedule design review for v2.0
|
||||
</li>
|
||||
</ul>
|
||||
<h2 className='mt-5 mb-3 border-[#E5E5E5] border-b pb-1.5 font-semibold text-[#1C1C1C] text-[16px]'>
|
||||
Discussion Points
|
||||
</h2>
|
||||
<p className='mb-3 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
The team agreed to prioritize the new onboarding flow. Key decisions:
|
||||
</p>
|
||||
<ol className='mb-3 list-decimal pl-[24px]'>
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Migrate to the new auth provider by end of March
|
||||
</li>
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Ship the dashboard redesign in two phases
|
||||
</li>
|
||||
<li className='mb-1 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Add automated testing for all critical paths
|
||||
</li>
|
||||
</ol>
|
||||
<h2 className='mt-5 mb-3 border-[#E5E5E5] border-b pb-1.5 font-semibold text-[#1C1C1C] text-[16px]'>
|
||||
Next Steps
|
||||
</h2>
|
||||
<p className='mb-3 text-[#1C1C1C] text-[14px] leading-[1.6]'>
|
||||
Follow up with engineering on the timeline for the API v2 migration. Draft the
|
||||
proposal for the board meeting next week.
|
||||
</p>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={MD_COMPONENTS}>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -809,8 +799,79 @@ const LOG_STATUS_STYLES: Record<string, { bg: string; text: string; label: strin
|
||||
error: { bg: '#FEE2E2', text: '#991B1B', label: 'Error' },
|
||||
}
|
||||
|
||||
interface MockLogDetail {
|
||||
output: string
|
||||
spans: { name: string; ms: number; depth: number }[]
|
||||
}
|
||||
|
||||
const MOCK_LOG_DETAILS: MockLogDetail[] = [
|
||||
{
|
||||
output: '{\n "result": "processed",\n "emails": 3,\n "status": "complete"\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 800, depth: 0 },
|
||||
{ name: 'search_web', ms: 210, depth: 1 },
|
||||
{ name: 'Function Block', ms: 180, depth: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
output: '{\n "score": 87,\n "label": "high",\n "confidence": 0.94\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 2100, depth: 0 },
|
||||
{ name: 'hubspot_get_contact', ms: 340, depth: 1 },
|
||||
{ name: 'Function Block', ms: 180, depth: 0 },
|
||||
{ name: 'Condition', ms: 50, depth: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
output: '{\n "error": "timeout",\n "message": "LLM request exceeded limit"\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 650, depth: 0 },
|
||||
{ name: 'search_kb', ms: 120, depth: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
output: '{\n "user": "james@globex.io",\n "steps_completed": 4,\n "status": "sent"\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 980, depth: 0 },
|
||||
{ name: 'send_email', ms: 290, depth: 1 },
|
||||
{ name: 'Function Block', ms: 210, depth: 0 },
|
||||
{ name: 'Agent Block', ms: 420, depth: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
output: '{\n "records_processed": 142,\n "inserted": 138,\n "errors": 4\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 1800, depth: 0 },
|
||||
{ name: 'salesforce_query', ms: 820, depth: 1 },
|
||||
{ name: 'Function Block', ms: 340, depth: 0 },
|
||||
{ name: 'Agent Block', ms: 1200, depth: 0 },
|
||||
{ name: 'insert_rows', ms: 610, depth: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
output: '{\n "result": "processed",\n "emails": 1,\n "status": "complete"\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 720, depth: 0 },
|
||||
{ name: 'gmail_read', ms: 190, depth: 1 },
|
||||
{ name: 'Function Block', ms: 160, depth: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
output: '{\n "ticket_id": "TKT-4291",\n "priority": "medium",\n "assigned": "support"\n}',
|
||||
spans: [
|
||||
{ name: 'Agent Block', ms: 1400, depth: 0 },
|
||||
{ name: 'classify_intent', ms: 380, depth: 1 },
|
||||
{ name: 'Function Block', ms: 220, depth: 0 },
|
||||
{ name: 'Agent Block', ms: 780, depth: 0 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const MOCK_LOG_DETAIL_MAX_MS = MOCK_LOG_DETAILS.map((d) => Math.max(...d.spans.map((s) => s.ms)))
|
||||
|
||||
function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
const [showSidebar, setShowSidebar] = useState(false)
|
||||
const [selectedRow, setSelectedRow] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (revealedRows < MOCK_LOG_DATA.length) return
|
||||
@@ -818,8 +879,6 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
return () => clearTimeout(timer)
|
||||
}, [revealedRows])
|
||||
|
||||
const selectedRow = 0
|
||||
|
||||
return (
|
||||
<div className='relative flex h-full'>
|
||||
<div className='flex min-w-0 flex-1 flex-col'>
|
||||
@@ -856,7 +915,11 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
className={isSelected ? 'bg-[#F5F5F5]' : 'hover:bg-[#FAFAFA]'}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
isSelected ? 'bg-[#F5F5F5]' : 'hover:bg-[#FAFAFA]'
|
||||
)}
|
||||
onClick={() => setSelectedRow(i)}
|
||||
>
|
||||
<td className='px-[24px] py-[10px] align-middle'>
|
||||
<span className='flex items-center gap-[12px] font-medium text-[#1C1C1C] text-[14px]'>
|
||||
@@ -908,24 +971,59 @@ function MockFullLogs({ revealedRows }: { revealedRows: number }) {
|
||||
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
|
||||
style={{ width: '45%' }}
|
||||
>
|
||||
<MockLogDetailsSidebar />
|
||||
<MockLogDetailsSidebar
|
||||
selectedRow={selectedRow}
|
||||
onPrev={() => setSelectedRow((r) => Math.max(0, r - 1))}
|
||||
onNext={() => setSelectedRow((r) => Math.min(MOCK_LOG_DATA.length - 1, r + 1))}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MockLogDetailsSidebar() {
|
||||
interface MockLogDetailsSidebarProps {
|
||||
selectedRow: number
|
||||
onPrev: () => void
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSidebarProps) {
|
||||
const row = MOCK_LOG_DATA[selectedRow]
|
||||
const detail = MOCK_LOG_DETAILS[selectedRow]
|
||||
const statusStyle = LOG_STATUS_STYLES[row[2]] ?? LOG_STATUS_STYLES.success
|
||||
const [date, time] = row[1].split(', ')
|
||||
const color = MOCK_LOG_COLORS[selectedRow]
|
||||
const maxMs = MOCK_LOG_DETAIL_MAX_MS[selectedRow]
|
||||
const isPrevDisabled = selectedRow === 0
|
||||
const isNextDisabled = selectedRow === MOCK_LOG_DATA.length - 1
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col px-[14px] pt-[12px]'>
|
||||
<div className='flex h-full flex-col overflow-y-auto px-[14px] pt-[12px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='font-medium text-[#1C1C1C] text-[14px]'>Log Details</span>
|
||||
<div className='flex items-center gap-[1px]'>
|
||||
<div className='flex h-[24px] w-[24px] items-center justify-center rounded-[4px] text-[#999] hover:bg-[#F5F5F5]'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onPrev}
|
||||
disabled={isPrevDisabled}
|
||||
className={cn(
|
||||
'flex h-[24px] w-[24px] items-center justify-center rounded-[4px] text-[#999]',
|
||||
isPrevDisabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-[#F5F5F5]'
|
||||
)}
|
||||
>
|
||||
<ChevronDown className='h-[14px] w-[14px] rotate-180' />
|
||||
</div>
|
||||
<div className='flex h-[24px] w-[24px] items-center justify-center rounded-[4px] text-[#999] hover:bg-[#F5F5F5]'>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
onClick={onNext}
|
||||
disabled={isNextDisabled}
|
||||
className={cn(
|
||||
'flex h-[24px] w-[24px] items-center justify-center rounded-[4px] text-[#999]',
|
||||
isNextDisabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-[#F5F5F5]'
|
||||
)}
|
||||
>
|
||||
<ChevronDown className='h-[14px] w-[14px]' />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -934,8 +1032,8 @@ function MockLogDetailsSidebar() {
|
||||
<div className='flex w-[120px] shrink-0 flex-col gap-[8px]'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Timestamp</span>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<span className='font-medium text-[#666] text-[13px]'>Mar 17</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>2:14 PM</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>{date}</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>{time}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-[8px]'>
|
||||
@@ -944,12 +1042,12 @@ function MockLogDetailsSidebar() {
|
||||
<div
|
||||
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
|
||||
style={{
|
||||
backgroundColor: '#7C3AED',
|
||||
borderColor: '#7C3AED60',
|
||||
backgroundColor: color,
|
||||
borderColor: `${color}60`,
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<span className='truncate font-medium text-[#666] text-[13px]'>Email Bot</span>
|
||||
<span className='truncate font-medium text-[#666] text-[13px]'>{row[0]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -957,26 +1055,52 @@ function MockLogDetailsSidebar() {
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-[8px]'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Level</span>
|
||||
<span className='inline-flex items-center rounded-full bg-[#DCFCE7] px-[8px] py-[2px] font-medium text-[#166534] text-[11px]'>
|
||||
Success
|
||||
<span
|
||||
className='inline-flex items-center rounded-full px-[8px] py-[2px] font-medium text-[11px]'
|
||||
style={{ backgroundColor: statusStyle.bg, color: statusStyle.text }}
|
||||
>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex h-[42px] items-center justify-between border-[#E5E5E5] border-b px-[8px]'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Trigger</span>
|
||||
<span className='rounded-[4px] bg-[#F5F5F5] px-[6px] py-[2px] text-[#666] text-[11px]'>
|
||||
API
|
||||
{row[4]}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex h-[42px] items-center justify-between px-[8px]'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Duration</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>1.2s</span>
|
||||
<span className='font-medium text-[#666] text-[13px]'>{row[5]}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[6px] rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-[10px] py-[8px]'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Workflow Output</span>
|
||||
<div className='rounded-[6px] bg-[#F0F0F0] p-[10px] font-mono text-[#555] text-[11px] leading-[1.5]'>
|
||||
{'{\n "result": "processed",\n "emails": 3,\n "status": "complete"\n}'}
|
||||
{detail.output}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[6px] rounded-[6px] border border-[#E5E5E5] bg-[#FAFAFA] px-[10px] py-[8px]'>
|
||||
<span className='font-medium text-[#999] text-[12px]'>Trace Spans</span>
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
{detail.spans.map((span, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn('flex flex-col gap-[3px]', span.depth === 1 && 'ml-[12px]')}
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='font-mono text-[#555] text-[11px]'>{span.name}</span>
|
||||
<span className='font-medium text-[#999] text-[11px]'>{span.ms}ms</span>
|
||||
</div>
|
||||
<div className='h-[4px] w-full overflow-hidden rounded-full bg-[#F0F0F0]'>
|
||||
<div
|
||||
className='h-full rounded-full bg-[#2F6FED]'
|
||||
style={{ width: `${(span.ms / maxMs) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -985,6 +1109,8 @@ function MockLogDetailsSidebar() {
|
||||
}
|
||||
|
||||
function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
const [selectedRow, setSelectedRow] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex h-[44px] shrink-0 items-center border-[#E5E5E5] border-b px-[24px]'>
|
||||
@@ -1037,26 +1163,48 @@ function MockFullTable({ revealedRows }: { revealedRows: number }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MOCK_TABLE_DATA.slice(0, revealedRows).map((row, i) => (
|
||||
<motion.tr
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
<td className='border-[#E5E5E5] border-r border-b px-[4px] py-[7px] text-center align-middle'>
|
||||
<span className='text-[#999] text-[11px] tabular-nums'>{i + 1}</span>
|
||||
</td>
|
||||
{row.map((cell, j) => (
|
||||
{MOCK_TABLE_DATA.slice(0, revealedRows).map((row, i) => {
|
||||
const isSelected = selectedRow === i
|
||||
return (
|
||||
<motion.tr
|
||||
key={i}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
className='cursor-pointer'
|
||||
onClick={() => setSelectedRow(i)}
|
||||
>
|
||||
<td
|
||||
key={j}
|
||||
className='border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle'
|
||||
className={cn(
|
||||
'border-[#E5E5E5] border-r border-b px-[4px] py-[7px] text-center align-middle',
|
||||
isSelected ? 'bg-[rgba(37,99,235,0.06)]' : 'hover:bg-[#FAFAFA]'
|
||||
)}
|
||||
>
|
||||
<span className='block truncate text-[#1C1C1C] text-[13px]'>{cell}</span>
|
||||
<span className='text-[#999] text-[11px] tabular-nums'>{i + 1}</span>
|
||||
</td>
|
||||
))}
|
||||
</motion.tr>
|
||||
))}
|
||||
{row.map((cell, j) => (
|
||||
<td
|
||||
key={j}
|
||||
className={cn(
|
||||
'relative border-[#E5E5E5] border-r border-b px-[8px] py-[7px] align-middle',
|
||||
isSelected ? 'bg-[rgba(37,99,235,0.06)]' : 'hover:bg-[#FAFAFA]'
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div
|
||||
className={cn(
|
||||
'-bottom-px -top-px pointer-events-none absolute left-0 z-[5] border-[#1a5cf6] border-t border-b',
|
||||
j === 0 && 'border-l',
|
||||
j === row.length - 1 ? '-right-px border-r' : 'right-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className='block truncate text-[#1C1C1C] text-[13px]'>{cell}</span>
|
||||
</td>
|
||||
))}
|
||||
</motion.tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,10 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
features: [
|
||||
'1,000 credits (trial)',
|
||||
'5GB file storage',
|
||||
'3 tables · 1,000 rows each',
|
||||
'5 min execution limit',
|
||||
'Limited log retention',
|
||||
'CLI/SDK Access',
|
||||
'7-day log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
@@ -36,11 +37,12 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
billingPeriod: 'per month',
|
||||
color: '#00F701',
|
||||
features: [
|
||||
'6,000 credits/mo',
|
||||
'+50 daily refresh credits',
|
||||
'150 runs/min (sync)',
|
||||
'50 min sync execution limit',
|
||||
'6,000 credits/mo · +50/day',
|
||||
'50GB file storage',
|
||||
'25 tables · 5,000 rows each',
|
||||
'50 min execution · 150 runs/min',
|
||||
'Unlimited log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
@@ -52,11 +54,12 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
billingPeriod: 'per month',
|
||||
color: '#FA4EDF',
|
||||
features: [
|
||||
'25,000 credits/mo',
|
||||
'+200 daily refresh credits',
|
||||
'300 runs/min (sync)',
|
||||
'50 min sync execution limit',
|
||||
'25,000 credits/mo · +200/day',
|
||||
'500GB file storage',
|
||||
'25 tables · 5,000 rows each',
|
||||
'50 min execution · 300 runs/min',
|
||||
'Unlimited log retention',
|
||||
'CLI/SDK/MCP Access',
|
||||
],
|
||||
cta: { label: 'Get started', href: '/signup' },
|
||||
},
|
||||
@@ -66,7 +69,15 @@ const PRICING_TIERS: PricingTier[] = [
|
||||
description: 'For organizations needing security and scale',
|
||||
price: 'Custom',
|
||||
color: '#FFCC02',
|
||||
features: ['Custom infra limits', 'SSO', 'SOC2', 'Self hosting', 'Dedicated support'],
|
||||
features: [
|
||||
'Custom credits & infra limits',
|
||||
'Custom file storage',
|
||||
'10,000 tables · 1M rows each',
|
||||
'Custom execution limits',
|
||||
'Unlimited log retention',
|
||||
'SSO & SCIM · SOC2 & HIPAA',
|
||||
'Self hosting · Dedicated support',
|
||||
],
|
||||
cta: { label: 'Book a demo', href: '/contact' },
|
||||
},
|
||||
]
|
||||
@@ -114,12 +125,12 @@ function PricingCard({ tier }: PricingCardProps) {
|
||||
</p>
|
||||
<div className='mt-4'>
|
||||
{isEnterprise ? (
|
||||
<a
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
|
||||
>
|
||||
{tier.cta.label}
|
||||
</a>
|
||||
</Link>
|
||||
) : isPro ? (
|
||||
<Link
|
||||
href={tier.cta.href}
|
||||
|
||||
@@ -28,8 +28,8 @@ import {
|
||||
* for immediate availability to AI crawlers.
|
||||
* - Section `id` attributes serve as fragment anchors for precise AI citations.
|
||||
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
|
||||
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration, Testimonials) ->
|
||||
* pricing (Pricing) -> enterprise (Enterprise).
|
||||
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration) ->
|
||||
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
|
||||
*/
|
||||
export default async function Landing() {
|
||||
return (
|
||||
@@ -43,8 +43,8 @@ export default async function Landing() {
|
||||
<Templates />
|
||||
<Features />
|
||||
<Collaboration />
|
||||
<Pricing />
|
||||
<Enterprise />
|
||||
<Pricing />
|
||||
<Testimonials />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
Reference in New Issue
Block a user