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
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:
+267
@@ -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>
|
||||
)
|
||||
}
|
||||
+169
@@ -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>
|
||||
)
|
||||
}
|
||||
+147
@@ -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
|
||||
})()
|
||||
Reference in New Issue
Block a user