chore: client and server components (#3716)

This commit is contained in:
Adithya Krishna
2026-03-23 10:41:18 -07:00
committed by GitHub
parent 91a0a49264
commit 694b53063b
25 changed files with 657 additions and 687 deletions
@@ -1,5 +1,3 @@
'use client'
import type { ReactNode } from 'react'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Navbar from '@/app/(home)/components/navbar/navbar'
@@ -0,0 +1,188 @@
'use client'
import { useRef, useState } from 'react'
import { motion, useInView } from 'framer-motion'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
interface PermissionFeature {
name: string
key: string
defaultEnabled: boolean
providerId?: string
}
interface PermissionCategory {
label: string
color: string
features: PermissionFeature[]
}
const PERMISSION_CATEGORIES: PermissionCategory[] = [
{
label: 'Providers',
color: '#FA4EDF',
features: [
{ key: 'openai', name: 'OpenAI', defaultEnabled: true, providerId: 'openai' },
{ key: 'anthropic', name: 'Anthropic', defaultEnabled: true, providerId: 'anthropic' },
{ key: 'google', name: 'Google', defaultEnabled: false, providerId: 'google' },
{ key: 'xai', name: 'xAI', defaultEnabled: true, providerId: 'xai' },
],
},
{
label: 'Workspace',
color: '#2ABBF8',
features: [
{ key: 'knowledge-base', name: 'Knowledge Base', defaultEnabled: true },
{ key: 'tables', name: 'Tables', defaultEnabled: true },
{ key: 'copilot', name: 'Copilot', defaultEnabled: false },
{ key: 'environment', name: 'Environment', defaultEnabled: false },
],
},
{
label: 'Tools',
color: '#33C482',
features: [
{ key: 'mcp-tools', name: 'MCP Tools', defaultEnabled: true },
{ key: 'custom-tools', name: 'Custom Tools', defaultEnabled: false },
{ key: 'skills', name: 'Skills', defaultEnabled: true },
{ key: 'invitations', name: 'Invitations', defaultEnabled: true },
],
},
]
const INITIAL_ACCESS_STATE = Object.fromEntries(
PERMISSION_CATEGORIES.flatMap((category) =>
category.features.map((feature) => [feature.key, feature.defaultEnabled])
)
)
function CheckboxIcon({ checked, color }: { checked: boolean; color: string }) {
return (
<div
className='h-[6px] w-[6px] shrink-0 rounded-full transition-colors duration-200'
style={{
backgroundColor: checked ? color : 'transparent',
border: checked ? 'none' : '1.5px solid #3A3A3A',
}}
/>
)
}
function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
if (!providerId) return null
const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
if (!ProviderIcon) return null
return (
<div className='relative flex h-[14px] w-[14px] shrink-0 items-center justify-center opacity-50 brightness-0 invert'>
<ProviderIcon className='!h-[14px] !w-[14px]' />
</div>
)
}
export function AccessControlPanel() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-40px' })
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
return (
<div ref={ref}>
<div className='lg:hidden'>
{PERMISSION_CATEGORIES.map((category, catIdx) => {
const offsetBefore = PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
)
return (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.05 + (offsetBefore + featIdx) * 0.04,
duration: 0.3,
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
</div>
</div>
)
})}
</div>
{/* Desktop -- categorized grid */}
<div className='hidden lg:block'>
{PERMISSION_CATEGORIES.map((category, catIdx) => (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
const currentIndex =
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
) + featIdx
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.1 + currentIndex * 0.04,
duration: 0.3,
ease: [0.25, 0.46, 0.45, 0.94],
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
</div>
</div>
))}
</div>
</div>
)
}
@@ -0,0 +1,226 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
/** 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.5, 0.35, 0.22, 0.12, 0.05] as const
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, 180_000, 360_000, 600_000]
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)
return (
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] 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>
<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>
</div>
</div>
)
}
export function AuditLogPreview() {
const counterRef = useRef(ENTRY_TEMPLATES.length)
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
const now = Date.now()
const [entries, setEntries] = useState<LogEntry[]>(() =>
ENTRY_TEMPLATES.slice(0, 6).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, 5),
])
}, 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='mt-5 overflow-hidden px-6 md:mt-6 md:px-8'>
<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>
)
}
@@ -12,127 +12,14 @@
* - `<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, useInView } 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'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
/** 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.5, 0.35, 0.22, 0.12, 0.05] as const
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, 180_000, 360_000, 600_000]
import { AccessControlPanel } from '@/app/(home)/components/enterprise/components/access-control-panel'
import { AuditLogPreview } from '@/app/(home)/components/enterprise/components/audit-log-preview'
const MARQUEE_KEYFRAMES = `
@keyframes marquee {
@@ -161,300 +48,6 @@ const FEATURE_TAGS = [
'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)
return (
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] 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>
<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>
</div>
</div>
)
}
function AuditLogPreview() {
const counterRef = useRef(ENTRY_TEMPLATES.length)
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
const now = Date.now()
const [entries, setEntries] = useState<LogEntry[]>(() =>
ENTRY_TEMPLATES.slice(0, 6).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, 5),
])
}, 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='mt-5 overflow-hidden px-6 md:mt-6 md:px-8'>
<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>
)
}
interface PermissionFeature {
name: string
key: string
defaultEnabled: boolean
providerId?: string
}
interface PermissionCategory {
label: string
color: string
features: PermissionFeature[]
}
const PERMISSION_CATEGORIES: PermissionCategory[] = [
{
label: 'Providers',
color: '#FA4EDF',
features: [
{ key: 'openai', name: 'OpenAI', defaultEnabled: true, providerId: 'openai' },
{ key: 'anthropic', name: 'Anthropic', defaultEnabled: true, providerId: 'anthropic' },
{ key: 'google', name: 'Google', defaultEnabled: false, providerId: 'google' },
{ key: 'xai', name: 'xAI', defaultEnabled: true, providerId: 'xai' },
],
},
{
label: 'Workspace',
color: '#2ABBF8',
features: [
{ key: 'knowledge-base', name: 'Knowledge Base', defaultEnabled: true },
{ key: 'tables', name: 'Tables', defaultEnabled: true },
{ key: 'copilot', name: 'Copilot', defaultEnabled: false },
{ key: 'environment', name: 'Environment', defaultEnabled: false },
],
},
{
label: 'Tools',
color: '#33C482',
features: [
{ key: 'mcp-tools', name: 'MCP Tools', defaultEnabled: true },
{ key: 'custom-tools', name: 'Custom Tools', defaultEnabled: false },
{ key: 'skills', name: 'Skills', defaultEnabled: true },
{ key: 'invitations', name: 'Invitations', defaultEnabled: true },
],
},
]
const INITIAL_ACCESS_STATE = Object.fromEntries(
PERMISSION_CATEGORIES.flatMap((category) =>
category.features.map((feature) => [feature.key, feature.defaultEnabled])
)
)
function CheckboxIcon({ checked, color }: { checked: boolean; color: string }) {
return (
<div
className='h-[6px] w-[6px] shrink-0 rounded-full transition-colors duration-200'
style={{
backgroundColor: checked ? color : 'transparent',
border: checked ? 'none' : '1.5px solid #3A3A3A',
}}
/>
)
}
function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
if (!providerId) return null
const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
if (!ProviderIcon) return null
return (
<div className='relative flex h-[14px] w-[14px] shrink-0 items-center justify-center opacity-50 brightness-0 invert'>
<ProviderIcon className='!h-[14px] !w-[14px]' />
</div>
)
}
function AccessControlPanel() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-40px' })
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
return (
<div ref={ref}>
<div className='lg:hidden'>
{PERMISSION_CATEGORIES.map((category, catIdx) => {
const offsetBefore = PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
)
return (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.05 + (offsetBefore + featIdx) * 0.04,
duration: 0.3,
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
</div>
</div>
)
})}
</div>
{/* Desktop — categorized grid */}
<div className='hidden lg:block'>
{PERMISSION_CATEGORIES.map((category, catIdx) => (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
const currentIndex =
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
) + featIdx
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.1 + currentIndex * 0.04,
duration: 0.3,
ease: [0.25, 0.46, 0.45, 0.94],
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
</div>
</div>
))}
</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'>
@@ -482,7 +75,7 @@ function TrustStrip() {
</div>
</Link>
{/* Open Source — center */}
{/* Open Source -- center */}
<Link
href='https://github.com/simstudioai/sim'
target='_blank'
@@ -1,5 +1,3 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
@@ -0,0 +1,238 @@
'use client'
import type { ComponentType, SVGProps } from 'react'
import { useState } from 'react'
import { createLogger } from '@sim/logger'
import {
ArrowRight,
ChevronRight,
Code2,
Database,
DollarSign,
HardDrive,
type LucideIcon,
RefreshCw,
Timer,
Zap,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
import { ENTERPRISE_PLAN_FEATURES } from '@/app/workspace/[workspaceId]/settings/components/subscription/plan-configs'
const logger = createLogger('LandingPricing')
interface PricingFeature {
icon: LucideIcon | ComponentType<SVGProps<SVGSVGElement>>
text: string
}
interface PricingTier {
name: string
tier: string
price: string
features: PricingFeature[]
ctaText: string
featured?: boolean
}
const FREE_PLAN_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '1,000 credits (trial)' },
{ icon: HardDrive, text: '5GB file storage' },
{ icon: Timer, text: '5 min execution limit' },
{ icon: Database, text: 'Limited log retention' },
{ icon: Code2, text: 'CLI/SDK Access' },
]
const PRO_LANDING_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '6,000 credits/mo' },
{ icon: RefreshCw, text: '+50 daily refresh credits' },
{ icon: Zap, text: '150 runs/min (sync)' },
{ icon: Timer, text: '50 min sync execution limit' },
{ icon: HardDrive, text: '50GB file storage' },
]
const MAX_LANDING_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '25,000 credits/mo' },
{ icon: RefreshCw, text: '+200 daily refresh credits' },
{ icon: Zap, text: '300 runs/min (sync)' },
{ icon: Timer, text: '50 min sync execution limit' },
{ icon: HardDrive, text: '500GB file storage' },
]
const pricingTiers: PricingTier[] = [
{
name: 'COMMUNITY',
tier: 'Free',
price: 'Free',
features: FREE_PLAN_FEATURES,
ctaText: 'Get Started',
},
{
name: 'PRO',
tier: 'Pro',
price: '$25/mo',
features: PRO_LANDING_FEATURES,
ctaText: 'Get Started',
featured: true,
},
{
name: 'MAX',
tier: 'Max',
price: '$100/mo',
features: MAX_LANDING_FEATURES,
ctaText: 'Get Started',
},
{
name: 'ENTERPRISE',
tier: 'Enterprise',
price: 'Custom',
features: ENTERPRISE_PLAN_FEATURES,
ctaText: 'Contact Sales',
},
]
function PricingCard({
tier,
isBeforeFeatured,
}: {
tier: PricingTier
isBeforeFeatured?: boolean
}) {
const [isHovered, setIsHovered] = useState(false)
const router = useRouter()
const handleCtaClick = () => {
logger.info(`Pricing CTA clicked: ${tier.name}`)
if (tier.ctaText === 'Contact Sales') {
window.open('https://form.typeform.com/to/jqCO12pF', '_blank')
} else {
router.push('/signup')
}
}
return (
<div
className={cn(
'relative flex h-full flex-col justify-between bg-[#FEFEFE]',
tier.featured ? 'p-0' : 'px-0 py-0',
'sm:px-5 sm:pt-4 sm:pb-4',
tier.featured
? 'sm:p-0'
: isBeforeFeatured
? 'sm:border-[#E7E4EF] sm:border-r-0'
: 'sm:border-[#E7E4EF] sm:border-r-2 sm:last:border-r-0',
!tier.featured && !isBeforeFeatured && 'lg:[&:nth-child(4n)]:border-r-0',
!tier.featured &&
!isBeforeFeatured &&
'sm:[&:nth-child(2n)]:border-r-0 lg:[&:nth-child(2n)]:border-r-2',
tier.featured ? 'z-10 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white' : ''
)}
>
<div
className={cn(
'flex h-full flex-col justify-between',
tier.featured
? 'border-2 border-[#6F3DFA] px-5 pt-4 pb-5 shadow-[inset_0_2px_4px_0_#9B77FF] sm:px-5 sm:pt-4 sm:pb-4'
: ''
)}
>
<div className='flex-1'>
<div className='mb-1'>
<span
className={cn(
'font-medium text-xs uppercase tracking-wider',
tier.featured ? 'text-white/90' : 'text-gray-500'
)}
>
{tier.name}
</span>
</div>
<div className='mb-6'>
<span
className={cn(
'font-medium text-4xl leading-none',
tier.featured ? 'text-white' : 'text-black'
)}
>
{tier.price}
</span>
</div>
<ul className='mb-[2px] space-y-3'>
{tier.features.map((feature, idx) => (
<li key={idx} className='flex items-start gap-2'>
<feature.icon
className={cn(
'mt-0.5 h-4 w-4 flex-shrink-0',
tier.featured ? 'text-white/90' : 'text-gray-600'
)}
/>
<span className={cn('text-sm', tier.featured ? 'text-white' : 'text-gray-700')}>
{feature.text}
</span>
</li>
))}
</ul>
</div>
<div className='mt-9'>
{tier.featured ? (
<button
onClick={handleCtaClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#E8E8E8] bg-gradient-to-b from-[#F8F8F8] to-white px-3 py-[6px] font-medium text-[#6F3DFA] text-[14px] shadow-[inset_0_2px_4px_0_rgba(255,255,255,0.9)] transition-all'
>
<span className='flex items-center gap-1'>
{tier.ctaText}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' />
) : (
<ChevronRight className='h-4 w-4' />
)}
</span>
</span>
</button>
) : (
<button
onClick={handleCtaClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#343434] bg-gradient-to-b from-[#060606] to-[#323232] px-3 py-[6px] font-medium text-[14px] text-white shadow-[inset_0_1.25px_2.5px_0_#9B77FF] transition-all'
>
<span className='flex items-center gap-1'>
{tier.ctaText}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' />
) : (
<ChevronRight className='h-4 w-4' />
)}
</span>
</span>
</button>
)}
</div>
</div>
</div>
)
}
/**
* Pricing grid with all tier cards. Rendered as a client component because
* the tier data contains component references (icon functions) which are
* not serializable across the RSC boundary.
*/
export function PricingGrid() {
return (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
{pricingTiers.map((tier, index) => {
const nextTier = pricingTiers[index + 1]
const isBeforeFeatured = nextTier?.featured
return <PricingCard key={tier.name} tier={tier} isBeforeFeatured={isBeforeFeatured} />
})}
</div>
)
}
@@ -1,226 +1,4 @@
'use client'
import type { ComponentType, SVGProps } from 'react'
import { useState } from 'react'
import { createLogger } from '@sim/logger'
import type { LucideIcon } from 'lucide-react'
import {
ArrowRight,
ChevronRight,
Code2,
Database,
DollarSign,
HardDrive,
RefreshCw,
Timer,
Zap,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
import { ENTERPRISE_PLAN_FEATURES } from '@/app/workspace/[workspaceId]/settings/components/subscription/plan-configs'
const logger = createLogger('LandingPricing')
interface PricingFeature {
icon: LucideIcon | ComponentType<SVGProps<SVGSVGElement>>
text: string
}
interface PricingTier {
name: string
tier: string
price: string
features: PricingFeature[]
ctaText: string
featured?: boolean
}
const FREE_PLAN_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '1,000 credits (trial)' },
{ icon: HardDrive, text: '5GB file storage' },
{ icon: Timer, text: '5 min execution limit' },
{ icon: Database, text: 'Limited log retention' },
{ icon: Code2, text: 'CLI/SDK Access' },
]
const PRO_LANDING_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '6,000 credits/mo' },
{ icon: RefreshCw, text: '+50 daily refresh credits' },
{ icon: Zap, text: '150 runs/min (sync)' },
{ icon: Timer, text: '50 min sync execution limit' },
{ icon: HardDrive, text: '50GB file storage' },
]
const MAX_LANDING_FEATURES: PricingFeature[] = [
{ icon: DollarSign, text: '25,000 credits/mo' },
{ icon: RefreshCw, text: '+200 daily refresh credits' },
{ icon: Zap, text: '300 runs/min (sync)' },
{ icon: Timer, text: '50 min sync execution limit' },
{ icon: HardDrive, text: '500GB file storage' },
]
const pricingTiers: PricingTier[] = [
{
name: 'COMMUNITY',
tier: 'Free',
price: 'Free',
features: FREE_PLAN_FEATURES,
ctaText: 'Get Started',
},
{
name: 'PRO',
tier: 'Pro',
price: '$25/mo',
features: PRO_LANDING_FEATURES,
ctaText: 'Get Started',
featured: true,
},
{
name: 'MAX',
tier: 'Max',
price: '$100/mo',
features: MAX_LANDING_FEATURES,
ctaText: 'Get Started',
},
{
name: 'ENTERPRISE',
tier: 'Enterprise',
price: 'Custom',
features: ENTERPRISE_PLAN_FEATURES,
ctaText: 'Contact Sales',
},
]
function PricingCard({
tier,
index,
isBeforeFeatured,
}: {
tier: PricingTier
index: number
isBeforeFeatured?: boolean
}) {
const [isHovered, setIsHovered] = useState(false)
const router = useRouter()
const handleCtaClick = () => {
logger.info(`Pricing CTA clicked: ${tier.name}`)
if (tier.ctaText === 'Contact Sales') {
window.open('https://form.typeform.com/to/jqCO12pF', '_blank')
} else {
router.push('/signup')
}
}
return (
<div
className={cn(
'relative flex h-full flex-col justify-between bg-[#FEFEFE]',
tier.featured ? 'p-0' : 'px-0 py-0',
'sm:px-5 sm:pt-4 sm:pb-4',
tier.featured
? 'sm:p-0'
: isBeforeFeatured
? 'sm:border-[#E7E4EF] sm:border-r-0'
: 'sm:border-[#E7E4EF] sm:border-r-2 sm:last:border-r-0',
!tier.featured && !isBeforeFeatured && 'lg:[&:nth-child(4n)]:border-r-0',
!tier.featured &&
!isBeforeFeatured &&
'sm:[&:nth-child(2n)]:border-r-0 lg:[&:nth-child(2n)]:border-r-2',
tier.featured ? 'z-10 bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white' : ''
)}
>
<div
className={cn(
'flex h-full flex-col justify-between',
tier.featured
? 'border-2 border-[#6F3DFA] px-5 pt-4 pb-5 shadow-[inset_0_2px_4px_0_#9B77FF] sm:px-5 sm:pt-4 sm:pb-4'
: ''
)}
>
<div className='flex-1'>
<div className='mb-1'>
<span
className={cn(
'font-medium text-xs uppercase tracking-wider',
tier.featured ? 'text-white/90' : 'text-gray-500'
)}
>
{tier.name}
</span>
</div>
<div className='mb-6'>
<span
className={cn(
'font-medium text-4xl leading-none',
tier.featured ? 'text-white' : 'text-black'
)}
>
{tier.price}
</span>
</div>
<ul className='mb-[2px] space-y-3'>
{tier.features.map((feature, idx) => (
<li key={idx} className='flex items-start gap-2'>
<feature.icon
className={cn(
'mt-0.5 h-4 w-4 flex-shrink-0',
tier.featured ? 'text-white/90' : 'text-gray-600'
)}
/>
<span className={cn('text-sm', tier.featured ? 'text-white' : 'text-gray-700')}>
{feature.text}
</span>
</li>
))}
</ul>
</div>
<div className='mt-9'>
{tier.featured ? (
<button
onClick={handleCtaClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#E8E8E8] bg-gradient-to-b from-[#F8F8F8] to-white px-3 py-[6px] font-medium text-[#6F3DFA] text-[14px] shadow-[inset_0_2px_4px_0_rgba(255,255,255,0.9)] transition-all'
>
<span className='flex items-center gap-1'>
{tier.ctaText}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' />
) : (
<ChevronRight className='h-4 w-4' />
)}
</span>
</span>
</button>
) : (
<button
onClick={handleCtaClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#343434] bg-gradient-to-b from-[#060606] to-[#323232] px-3 py-[6px] font-medium text-[14px] text-white shadow-[inset_0_1.25px_2.5px_0_#9B77FF] transition-all'
>
<span className='flex items-center gap-1'>
{tier.ctaText}
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
{isHovered ? (
<ArrowRight className='h-4 w-4' />
) : (
<ChevronRight className='h-4 w-4' />
)}
</span>
</span>
</button>
)}
</div>
</div>
</div>
)
}
import { PricingGrid } from '@/app/(landing)/components/landing-pricing/components/pricing-card'
/**
* Landing page pricing section displaying tiered pricing plans
@@ -230,20 +8,7 @@ export default function LandingPricing() {
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-[4px]' aria-label='Pricing plans'>
<h2 className='sr-only'>Pricing Plans</h2>
<div className='relative mx-auto w-full max-w-[1289px]'>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
{pricingTiers.map((tier, index) => {
const nextTier = pricingTiers[index + 1]
const isBeforeFeatured = nextTier?.featured
return (
<PricingCard
key={tier.name}
tier={tier}
index={index}
isBeforeFeatured={isBeforeFeatured}
/>
)
})}
</div>
<PricingGrid />
</div>
</section>
)
@@ -1,5 +1,3 @@
'use client'
import { Skeleton } from '@/components/emcn'
export function ChatLoadingState() {
@@ -1,5 +1,3 @@
'use client'
import { Skeleton } from '@/components/emcn'
import AuthBackground from '@/app/(auth)/components/auth-background'
@@ -1,5 +1,3 @@
'use client'
import { CheckCircle2 } from 'lucide-react'
interface ThankYouScreenProps {
@@ -1,5 +1,3 @@
'use client'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
-2
View File
@@ -1,5 +1,3 @@
'use client'
import { season } from '@/app/_styles/fonts/season/season'
export default function TemplatesLayoutClient({ children }: { children: React.ReactNode }) {
@@ -1,5 +1,3 @@
'use client'
import type { ReactNode } from 'react'
import { Blimp } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
@@ -1,5 +1,3 @@
'use client'
import type { ResourceCell } from '@/app/workspace/[workspaceId]/components/resource/resource'
import type { WorkspaceMember } from '@/hooks/queries/workspace'
@@ -1,5 +1,3 @@
'use client'
import { getDocumentIcon } from '@/components/icons/document-icons'
import { cn } from '@/lib/core/utils/cn'
import type { ChatMessageAttachment } from '../types'
@@ -1,5 +1,3 @@
'use client'
import { PillsRing } from '@/components/emcn'
import type { ToolCallStatus } from '../../../../types'
import { getToolIcon } from '../../utils'
@@ -1,5 +1,3 @@
'use client'
import { ToastProvider } from '@/components/emcn'
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
@@ -1,5 +1,3 @@
'use client'
import { CredentialsManager } from '@/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager'
export function Credentials() {
@@ -1,5 +1,3 @@
'use client'
import { IntegrationsManager } from '@/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager'
export function Integrations() {
@@ -1,5 +1,3 @@
'use client'
import type { ReactNode } from 'react'
import { splitReferenceSegment } from '@/lib/workflows/sanitization/references'
import { normalizeName, REFERENCE } from '@/executor/constants'
@@ -1,5 +1,3 @@
'use client'
import type * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/core/utils/cn'
@@ -1,5 +1,3 @@
'use client'
import type * as React from 'react'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
@@ -1,5 +1,3 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/core/utils/cn'
@@ -1,5 +1,3 @@
'use client'
interface SearchHighlightProps {
text: string
searchQuery: string
-2
View File
@@ -1,5 +1,3 @@
'use client'
import { Code } from '@/components/emcn'
interface CodeBlockProps {