chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,267 @@
'use client'
import { useEffect, useState } from 'react'
import { ChevronDown, cn } from '@sim/emcn'
import {
ArrowRight,
ArrowUp,
ClipboardList,
Files,
Mic,
Paperclip,
Plus,
ShieldCheck,
Shuffle,
Slash,
Table,
} from '@sim/emcn/icons'
import { ThinkingLoader } from '@/components/ui'
import {
COMPOSER_PLACEHOLDER,
ENTERPRISE_GREETING,
ENTERPRISE_PROMPT,
ENTERPRISE_REPLY,
type EnterpriseLoopPhase,
PROMPT_CHAR_MS,
REPLY_WORD_MS,
SUGGESTED_ACTIONS,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
const REPLY_WORDS = ENTERPRISE_REPLY.split(' ')
/**
* Reveals an incrementing count (typed chars, streamed words) at a fixed
* step interval while `active`, deriving progress from ELAPSED time so a
* throttled background tab catches up instead of stalling mid-reveal.
* Resets to 0 when inactive; jumps straight to `total` under
* `prefers-reduced-motion`.
*/
function useElapsedReveal(active: boolean, stepMs: number, total: number) {
const [revealed, setRevealed] = useState(0)
useEffect(() => {
if (!active) {
setRevealed(0)
return
}
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
let interval: ReturnType<typeof setInterval> | null = null
const run = () => {
const startedAt = performance.now()
interval = setInterval(() => {
const elapsed = performance.now() - startedAt
const n = Math.min(Math.floor(elapsed / stepMs) + 1, total)
setRevealed(n)
if (n >= total && interval) clearInterval(interval)
}, stepMs)
}
const syncMotionPreference = () => {
if (interval) clearInterval(interval)
if (media.matches) {
setRevealed(total)
return
}
run()
}
syncMotionPreference()
media.addEventListener('change', syncMotionPreference)
return () => {
media.removeEventListener('change', syncMotionPreference)
if (interval) clearInterval(interval)
}
}, [active, stepMs, total])
return revealed
}
/** Greyscale leading icons for the suggested-action rows, in row order. */
const ACTION_ICONS = [Table, ShieldCheck, ClipboardList, Files] as const
const Caret = () => (
<span
aria-hidden='true'
className='ml-px inline-block h-[16px] w-px translate-y-[2px] animate-caret-blink bg-[var(--text-primary)]'
/>
)
interface ComposerProps {
/** Rendered in the text region (placeholder span or typed prompt). */
children: React.ReactNode
/** Fills the send disc with the active ink once the prompt has text. */
active: boolean
}
/**
* The Mothership composer chrome - white rounded field, text region on top,
* icon rail beneath (add / attach / skills left, mic + send disc right) -
* matching the homepage loop's composer and the real `UserInput`.
*/
function Composer({ children, active }: ComposerProps) {
return (
<div className='w-full rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 shadow-[0_1px_2px_0_rgba(18,18,18,0.05)]'>
<p className='min-h-[24px] px-1.5 pt-1 text-[15px] text-[var(--text-primary)] leading-[24px]'>
{children}
</p>
<div className='mt-2 flex items-center gap-1.5'>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Plus className='size-[16px] text-[var(--text-icon)]' />
</span>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Paperclip className='size-[16px] text-[var(--text-icon)]' />
</span>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Slash className='size-[16px] text-[var(--text-icon)]' />
</span>
<span className='ml-auto flex items-center gap-1.5'>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Mic className='size-[16px] text-[var(--text-icon)]' />
</span>
<span
className={cn(
'flex size-[28px] items-center justify-center rounded-full transition-colors duration-200',
active ? 'bg-[#383838]' : 'bg-[#808080]'
)}
>
<ArrowUp className='size-[16px] text-white' />
</span>
</span>
</div>
</div>
)
}
interface EnterpriseHomeStageProps {
/** Current beat, driven by the parent {@link EnterprisePlatformLoop} clock. */
phase: EnterpriseLoopPhase
/** True during the brief fade-out before the cycle restarts. */
fading: boolean
}
/**
* The main pane of the enterprise loop - the real workspace's NEW-CHAT home
* view, replayed: the centered greeting, the composer (placeholder, then the
* enterprise prompt typing out, then the send disc arming), and the
* "Suggested actions" rows beneath. On `dispatch` the home layer exits first
* and the conversation layer (the sent prompt as a user bubble over the goo
* {@link ThinkingLoader}, with the composer docked at the bottom) enters after
* a beat; the loader thinks while the parent's stage pane builds the workflow,
* then the reply streams in word by word on `reply`.
*
* Purely presentational; the clock lives in the parent so the chat and the
* workflow stage animate off one timeline. Both typewriters derive from
* ELAPSED time so throttled background tabs catch up instead of stalling
* mid-sentence.
*/
export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps) {
const isTyping = phase === 'typing'
const isReply = phase === 'reply'
const inConversation = phase === 'dispatch' || isReply
const promptDone = phase === 'typed' || inConversation
const typedChars = useElapsedReveal(isTyping, PROMPT_CHAR_MS, ENTERPRISE_PROMPT.length)
const revealedWords = useElapsedReveal(isReply, REPLY_WORD_MS, REPLY_WORDS.length)
const visiblePrompt = promptDone ? ENTERPRISE_PROMPT : ENTERPRISE_PROMPT.slice(0, typedChars)
const hasText = visiblePrompt.length > 0
return (
<div
className={cn(
'relative h-full w-full bg-[var(--bg)] transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
{/* Home layer - greeting, composer, suggested actions. Exits FIRST on
dispatch (no delay) so the swap never reads as two stacked layers. */}
<div
className={cn(
'absolute inset-0 flex flex-col items-center justify-center px-10 pb-[6vh] transition-opacity duration-200 ease-out',
inConversation ? 'pointer-events-none opacity-0' : 'opacity-100'
)}
>
<p className='mb-7 text-[30px] text-[var(--text-primary)]'>{ENTERPRISE_GREETING}</p>
<div className='w-full max-w-[576px]'>
<Composer active={hasText}>
{hasText ? (
<>
{visiblePrompt}
{isTyping && <Caret />}
</>
) : (
<span className='font-[380] text-[var(--text-muted)]'>{COMPOSER_PLACEHOLDER}</span>
)}
</Composer>
<div className='mt-7'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-2'>
<span className='text-[13px] text-[var(--text-muted)]'>Suggested actions</span>
<ChevronDown className='h-[7px] w-[9px] text-[var(--text-muted)]' />
</span>
<span className='flex items-center gap-1.5'>
<span className='text-[13px] text-[var(--text-muted)]'>Shuffle</span>
<Shuffle className='size-[14px] text-[var(--text-muted)]' />
</span>
</div>
<div className='mt-2 flex flex-col'>
{SUGGESTED_ACTIONS.map((action, i) => {
const Icon = ACTION_ICONS[i]
return (
<span
key={action}
className={cn(
'flex items-center gap-2 border-[var(--border-1)] px-2 py-2',
i > 0 && 'border-t'
)}
>
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-muted)]' />
<span className='flex-1 truncate text-[var(--text-primary)] text-sm'>
{action}
</span>
<ArrowRight className='size-[16px] shrink-0 text-[var(--text-muted)]' />
</span>
)
})}
</div>
</div>
</div>
</div>
{/* Conversation layer - the sent exchange. Enters AFTER the home layer's
200ms exit so the handoff is a choreographed swap, not a crossfade.
The loader thinks through the workflow build, then the reply streams. */}
<div
className={cn(
'absolute inset-0 flex flex-col transition-opacity duration-200 ease-out',
inConversation ? 'opacity-100 [transition-delay:220ms]' : 'pointer-events-none opacity-0'
)}
>
<div className='mx-auto flex min-h-0 w-full max-w-[640px] flex-1 flex-col gap-6 overflow-hidden px-6 pt-6'>
<div className='max-w-[82%] self-end rounded-2xl bg-[var(--surface-3)] px-4 py-3 text-[15px] text-[var(--text-primary)] leading-[1.5]'>
{ENTERPRISE_PROMPT}
</div>
{phase === 'dispatch' && (
<ThinkingLoader size={26} phase labelRatio={0.58} className='mt-1' />
)}
<p
className={cn(
'text-[15px] text-[var(--text-primary)] leading-[1.6] transition-opacity duration-200 ease-out',
isReply ? 'opacity-100' : 'opacity-0'
)}
>
{REPLY_WORDS.slice(0, revealedWords).join(' ')}
</p>
</div>
<div className='mx-auto mb-5 w-[calc(100%-40px)] max-w-[600px] shrink-0'>
<Composer active={false}>
<span className='font-[380] text-[var(--text-muted)]'>Send message to Sim</span>
</Composer>
</div>
</div>
</div>
)
}
@@ -0,0 +1,169 @@
'use client'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage'
import { EnterpriseSidebar } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
import {
BUILD_STEP_MS,
ENTERPRISE_STAGE_BLOCKS,
ENTERPRISE_STAGE_CANVAS,
ENTERPRISE_STAGE_EDGES,
type EnterpriseLoopPhase,
LOOP_TIMELINE,
RESET_FADE_MS,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
/**
* The window interior's design space, matching the homepage loop's capture
* geometry (the 2560x1470 shot is a 1280x735 CSS layout shown in the 1080x620
* window, so the app's native type reads at the same ~84.4% "mini app" scale):
* the sidebar column is 249px, and the workspace container is inset 7-8px.
*/
const DESIGN = { width: 1280, height: 735 } as const
/**
* The enterprise hero's platform loop - a sibling of the homepage
* `HeroPlatformLoop` that shares its architecture (fixed design-space layer
* scaled to the window via ResizeObserver + `transform: scale`, a parent-owned
* timeline clock driving presentational stages, reduced-motion showing a
* static finished frame) but diverges in content: where the homepage overlays
* a live chat over a baked screenshot, this variant renders the WHOLE interior
* live - the {@link EnterpriseSidebar} (a filled-out Brightwave workspace) and
* the {@link EnterpriseHomeStage} (the real new-chat home view, replaying an
* enterprise prompt) - because its sidebar content differs from the shot's.
*
* Timeline (see `stage-data.ts` - later stages append beats there): idle
* new-chat view → prompt types out → send arms → dispatch (user bubble +
* thinking, full-width) → the stage pane slides in from the right (the real
* `MothershipView` `w-0 ↔ w-1/2` width transition) → the invoice workflow
* assembles block by block (the shared {@link HeroWorkflowStage}, staged with
* the enterprise flow) → the reply streams in → hold → fade → restart.
*
* Everything is `pointer-events-none` decorative, matching the hero's
* `aria-hidden` frame. Under `prefers-reduced-motion` the loop never starts:
* the finished exchange, open stage, and fully-built workflow render
* statically.
*/
export function EnterprisePlatformLoop() {
const regionRef = useRef<HTMLDivElement>(null)
const [phase, setPhase] = useState<EnterpriseLoopPhase>('idle')
const [stageOpen, setStageOpen] = useState(false)
const [builtCount, setBuiltCount] = useState(0)
const [fading, setFading] = useState(false)
const [cycleId, setCycleId] = useState(0)
const [scale, setScale] = useState(1)
// Track the rendered region width and scale the design-space layer to fill
// it, keeping the live layer's proportions locked to the window's.
useLayoutEffect(() => {
const el = regionRef.current
if (!el) return
const measure = () => {
const w = el.getBoundingClientRect().width
if (w > 40) setScale(w / DESIGN.width)
}
measure()
const ro = new ResizeObserver(measure)
ro.observe(el)
return () => ro.disconnect()
}, [])
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
let timers: ReturnType<typeof setTimeout>[] = []
const clearScheduled = () => {
timers.forEach(clearTimeout)
timers = []
}
const showFinished = () => {
clearScheduled()
setFading(false)
setPhase('reply')
setStageOpen(true)
setBuiltCount(ENTERPRISE_STAGE_BLOCKS.length)
}
const runCycle = () => {
setFading(false)
setPhase('idle')
setStageOpen(false)
setBuiltCount(0)
setCycleId((c) => c + 1)
timers = [
setTimeout(() => setPhase('typing'), LOOP_TIMELINE.typing),
setTimeout(() => setPhase('typed'), LOOP_TIMELINE.typed),
setTimeout(() => setPhase('dispatch'), LOOP_TIMELINE.dispatch),
setTimeout(() => setStageOpen(true), LOOP_TIMELINE.stageOpen),
...ENTERPRISE_STAGE_BLOCKS.map((_, i) =>
setTimeout(() => setBuiltCount(i + 1), LOOP_TIMELINE.buildStart + i * BUILD_STEP_MS)
),
setTimeout(() => setPhase('reply'), LOOP_TIMELINE.reply),
setTimeout(() => setFading(true), LOOP_TIMELINE.total - RESET_FADE_MS),
setTimeout(runCycle, LOOP_TIMELINE.total),
]
}
const syncMotionPreference = () => {
clearScheduled()
if (media.matches) {
showFinished()
return
}
runCycle()
}
syncMotionPreference()
media.addEventListener('change', syncMotionPreference)
return () => {
media.removeEventListener('change', syncMotionPreference)
clearScheduled()
}
}, [])
return (
<div ref={regionRef} className='pointer-events-none absolute inset-0 overflow-hidden'>
<div
className='flex origin-top-left bg-[var(--surface-1)]'
style={{
width: DESIGN.width,
height: DESIGN.height,
transform: `scale(${scale})`,
}}
>
<EnterpriseSidebar />
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>
<div className='flex h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div className='relative h-full min-w-0 flex-1'>
<EnterpriseHomeStage phase={phase} fading={fading} />
</div>
<div
className={cn(
'h-full shrink-0 overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]',
stageOpen ? 'w-1/2 border-l' : 'w-0 min-w-0 border-l-0'
)}
>
<div
className={cn(
'h-full w-full transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<HeroWorkflowStage
key={cycleId}
builtCount={builtCount}
blocks={ENTERPRISE_STAGE_BLOCKS}
edges={ENTERPRISE_STAGE_EDGES}
canvas={ENTERPRISE_STAGE_CANVAS}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,147 @@
import { ChevronDown, cn, Home, Library } from '@sim/emcn'
import {
Calendar,
Database,
File,
HelpCircle,
Integration,
MoreHorizontal,
PanelLeft,
Plus,
Search,
Settings,
Table,
} from '@sim/emcn/icons'
import Image from 'next/image'
import {
SIDEBAR_CHATS,
SIDEBAR_WORKFLOWS,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
const WORKSPACE_NAV = [
{ label: 'Tables', icon: Table },
{ label: 'Files', icon: File },
{ label: 'Knowledge base', icon: Database },
{ label: 'Scheduled tasks', icon: Calendar },
{ label: 'Logs', icon: Library },
] as const
interface IconRowProps {
icon: React.ComponentType<{ className?: string }>
label: string
active?: boolean
}
/** A sidebar nav row with a leading icon, like the real workspace sidebar. */
function IconRow({ icon: Icon, label, active = false }: IconRowProps) {
return (
<div
className={cn(
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2',
active && 'bg-[var(--surface-active)]'
)}
>
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate font-[450] text-[13px] text-[var(--text-body)]'>{label}</span>
</div>
)
}
/** A bare text row - the real sidebar's chat and workflow entries. */
function TextRow({ label }: { label: string }) {
return (
<div className='mx-0.5 flex h-[28px] items-center rounded-[8px] px-2'>
<span className='truncate font-[450] text-[13px] text-[var(--text-body)]'>{label}</span>
</div>
)
}
/** Muted section heading (Chats / Workspace / Workflows). */
function SectionLabel({ label, actions }: { label: string; actions?: boolean }) {
return (
<div className='flex items-center justify-between px-4 pb-1.5'>
<span className='text-[12px] text-[var(--text-icon)]'>{label}</span>
{actions && (
<span className='flex items-center gap-2 text-[var(--text-icon)]'>
<MoreHorizontal className='size-[14px]' />
<Plus className='size-[14px]' />
</span>
)}
</div>
)
}
/**
* The Brightwave workspace sidebar, rendered live (the homepage loop keeps its
* baked-screenshot sidebar; the enterprise loop draws its own so the content
* can read like a large tenured deployment): the workspace header, New chat /
* Search / Integrations, a filled-out Chats history, the Workspace nav, a full
* Workflows section, and the Help / Settings footer. Purely decorative -
* hover/click behavior is owned by the parent's `pointer-events-none` frame.
*/
export function EnterpriseSidebar() {
return (
<div className='flex h-full w-[249px] flex-shrink-0 flex-col bg-[var(--surface-1)] pt-3'>
{/* Workspace header, matching the real product's WorkspaceHeader chip
(borderless `chipVariants()` geometry: h-[30px] rounded-lg px-2 with
mx-0.5, 16px logo, text-sm name, 6x10 chevron) and therefore the
homepage's baked sidebar pixels - logo + name + chevron as a bare
row, panel toggle right-aligned outside it. */}
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='mx-0.5 flex h-[30px] min-w-0 items-center gap-2 rounded-lg px-2'>
{/* The exact Brightwave mark the homepage capture seeds
(`readme-tour-capture` sets `logoUrl: '/landing/rivian-logo.svg'`),
so both platform previews show the same company logo. */}
<Image
src='/landing/rivian-logo.svg'
alt=''
width={16}
height={16}
className='size-[16px] flex-shrink-0 rounded-sm'
/>
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>Brightwave</span>
<ChevronDown className='h-[6px] w-[10px] flex-shrink-0 text-[var(--text-icon)]' />
</div>
<PanelLeft className='mr-1.5 size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
</div>
<div className='mt-2.5 flex flex-shrink-0 flex-col gap-0.5 px-2'>
<IconRow icon={Home} label='New chat' active />
<IconRow icon={Search} label='Search' />
<IconRow icon={Integration} label='Integrations' />
</div>
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<SectionLabel label='Chats' />
<div className='flex flex-col gap-0.5 px-2'>
{SIDEBAR_CHATS.map((chat) => (
<TextRow key={chat} label={chat} />
))}
</div>
</div>
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<SectionLabel label='Workspace' />
<div className='flex flex-col gap-0.5 px-2'>
{WORKSPACE_NAV.map((item) => (
<IconRow key={item.label} icon={item.icon} label={item.label} />
))}
</div>
</div>
<div className='flex min-h-0 flex-1 flex-col overflow-hidden pt-3.5'>
<SectionLabel label='Workflows' actions />
<div className='flex flex-col gap-0.5 px-2'>
{SIDEBAR_WORKFLOWS.map((workflow) => (
<TextRow key={workflow} label={workflow} />
))}
</div>
</div>
<div className='flex flex-shrink-0 flex-col gap-0.5 px-2 pt-[9px] pb-2'>
<IconRow icon={HelpCircle} label='Help' />
<IconRow icon={Settings} label='Settings' />
</div>
</div>
)
}
@@ -0,0 +1,3 @@
export { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage'
export { EnterprisePlatformLoop } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-platform-loop'
export { EnterpriseSidebar } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
@@ -0,0 +1,180 @@
import { AgentIcon, ConditionalIcon, MailIcon, StartIcon, TableIcon } from '@/components/icons'
import type { BlockDef } from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
/**
* Content + timing config for the enterprise hero's platform loop. Kept in one
* place (and phase starts derived, not hardcoded) so later stages - tables,
* files, knowledge base, logs walkthroughs - extend the timeline by appending
* beats here rather than reworking the clock.
*/
/** The new-chat greeting, personalized like the real workspace Home. */
export const ENTERPRISE_GREETING = 'What should we get done, Morgan?'
/** Placeholder shown in the composer before the prompt types out. */
export const COMPOSER_PLACEHOLDER = 'Ask Sim to automate a process.'
/**
* The prompt the loop types - a large company's multi-system operational
* workflow (AP + NetSuite + finance review + audit trail), concise enough to
* type on screen.
*/
export const ENTERPRISE_PROMPT =
'When a vendor invoice lands in AP, match it against the PO in NetSuite, flag exceptions for finance review, and log the approval trail.'
/** Typewriter cadence for the composer prompt. */
export const PROMPT_CHAR_MS = 28
/** Sim's reply, streamed word by word once the workflow finishes building. */
export const ENTERPRISE_REPLY =
"On it. I'll match each AP invoice to its PO in NetSuite, route exceptions to finance review, and log the full approval trail."
/** Word-reveal cadence for the streamed reply. */
export const REPLY_WORD_MS = 55
/**
* Recent chats - enterprise-flavored, so Brightwave reads long-tenured. Four
* entries: together with the five workflows this fills the sidebar's 735px
* design height exactly, without clipping the Workflows section.
*/
export const SIDEBAR_CHATS = [
'Vendor invoice exceptions',
'Q3 access review',
'NetSuite sync errors',
'Supplier onboarding docs',
] as const
/** Deployed workflows - a fuller section than the homepage's three. */
export const SIDEBAR_WORKFLOWS = [
'Invoice exception routing',
'Employee onboarding',
'Vendor risk scoring',
'IT access provisioning',
'Weekly compliance report',
] as const
/** Suggested actions under the composer, mirroring the real Home rows. */
export const SUGGESTED_ACTIONS = [
'Reconcile vendor invoices in NetSuite',
'Triage pending IT access requests',
'Summarize open compliance exceptions',
'Draft the weekly ops readiness report',
] as const
/**
* The workflow the chat "builds" on the stage pane - the invoice-matching flow
* the prompt describes, distilled to what reads at mini-app scale: Start feeds
* the NetSuite PO match, exceptions are flagged, and the flow fans out to
* finance review and the audit log. Same geometry conventions as the homepage
* stage (250px blocks, vertical spine at x=155, terminals fanned at y=580);
* tiles use the platform's grey text ramp - color is reserved for real
* third-party marks, and none of these carry one.
*
* Ordered by build sequence; an edge draws once both endpoints are on canvas.
*/
export const ENTERPRISE_STAGE_BLOCKS: BlockDef[] = [
{
id: 'start',
name: 'Start',
icon: StartIcon,
bgColor: 'var(--text-muted)',
isTrigger: true,
rows: [{ title: 'Inputs', value: '-' }],
x: 155,
y: 12,
},
{
id: 'match',
name: 'Match PO',
icon: AgentIcon,
bgColor: 'var(--text-primary)',
rows: [
{ title: 'Messages', value: '-' },
{ title: 'Model', value: '-' },
],
x: 155,
y: 172,
},
{
id: 'exceptions',
name: 'Flag exceptions',
icon: ConditionalIcon,
bgColor: 'var(--text-secondary)',
rows: [{ title: 'Conditions', value: '-' }],
x: 155,
y: 372,
},
{
id: 'review',
name: 'Finance review',
icon: MailIcon,
bgColor: 'var(--text-body)',
isTerminal: true,
rows: [
{ title: 'To', value: '-' },
{ title: 'Subject', value: '-' },
],
x: 0,
y: 560,
},
{
id: 'audit',
name: 'Audit log',
icon: TableIcon,
bgColor: 'var(--text-muted)',
isTerminal: true,
rows: [
{ title: 'Table', value: '-' },
{ title: 'Operation', value: '-' },
],
x: 310,
y: 560,
},
]
/** Source → target pairs, drawn in order as their endpoints land on canvas. */
export const ENTERPRISE_STAGE_EDGES: ReadonlyArray<readonly [string, string]> = [
['start', 'match'],
['match', 'exceptions'],
['exceptions', 'review'],
['exceptions', 'audit'],
]
/** Design-space bounding box of the layout above. */
export const ENTERPRISE_STAGE_CANVAS = { width: 560, height: 680 } as const
/** Where the main pane is within one loop pass. */
export type EnterpriseLoopPhase = 'idle' | 'typing' | 'typed' | 'dispatch' | 'reply'
/** The idle new-chat view holds this long before typing starts. */
const IDLE_HOLD_MS = 1400
/** Rest on the fully-typed prompt before "send". */
const TYPED_HOLD_MS = 700
/** Thinking runs alone this long before the stage pane slides open. */
const STAGE_OPEN_AFTER_MS = 900
/** First block lands this long after the pane opens. */
const BUILD_START_AFTER_MS = 500
/** Block N (build order) pops in at buildStart + N * BUILD_STEP_MS. */
export const BUILD_STEP_MS = 620
/** The reply starts streaming this long after the last block lands. */
const REPLY_AFTER_MS = 500
/** The finished scene (reply + built canvas) holds this long. */
const REPLY_HOLD_MS = 4800
/** Fade-out length before the cycle restarts. */
export const RESET_FADE_MS = 300
/**
* Derived phase starts. Later stages (tables, files, knowledge base, logs)
* slot in after `reply` by extending {@link EnterpriseLoopPhase} and appending
* starts here.
*/
export const LOOP_TIMELINE = (() => {
const typing = IDLE_HOLD_MS
const typed = typing + ENTERPRISE_PROMPT.length * PROMPT_CHAR_MS
const dispatch = typed + TYPED_HOLD_MS
const stageOpen = dispatch + STAGE_OPEN_AFTER_MS
const buildStart = stageOpen + BUILD_START_AFTER_MS
const reply = buildStart + (ENTERPRISE_STAGE_BLOCKS.length - 1) * BUILD_STEP_MS + REPLY_AFTER_MS
const total = reply + REPLY_HOLD_MS
return { typing, typed, dispatch, stageOpen, buildStart, reply, total } as const
})()