Files
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

362 lines
14 KiB
TypeScript

'use client'
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
import { AnimatePresence, domAnimation, LazyMotion, m, type Variants } from 'framer-motion'
import { LandingPreviewChat } from '@/app/(landing)/components/landing-preview/components/landing-preview-chat/landing-preview-chat'
import { LandingPreviewFiles } from '@/app/(landing)/components/landing-preview/components/landing-preview-files/landing-preview-files'
import { LandingPreviewHome } from '@/app/(landing)/components/landing-preview/components/landing-preview-home/landing-preview-home'
import { LandingPreviewKnowledge } from '@/app/(landing)/components/landing-preview/components/landing-preview-knowledge/landing-preview-knowledge'
import { LandingPreviewLogs } from '@/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs'
import { LandingPreviewScheduledTasks } from '@/app/(landing)/components/landing-preview/components/landing-preview-scheduled-tasks/landing-preview-scheduled-tasks'
import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
import { LandingPreviewSidebar } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'
import { LandingPreviewStageHeader } from '@/app/(landing)/components/landing-preview/components/landing-preview-stage/landing-preview-stage-header'
import { LandingPreviewTables } from '@/app/(landing)/components/landing-preview/components/landing-preview-tables/landing-preview-tables'
import { LandingPreviewWorkflow } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/landing-preview-workflow'
import {
EASE_OUT,
getViewChat,
getWorkflowStepDuration,
PREVIEW_WORKFLOWS,
} from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
/** Chat-switcher breadcrumb title per non-workflow staged view. */
const CHAT_TITLES: Partial<Record<SidebarView, string>> = {
logs: 'Agent activity',
tables: 'Workspace data',
files: 'Files',
knowledge: 'Knowledge base',
'scheduled-tasks': 'Scheduled tasks',
}
const containerVariants: Variants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.15 },
},
}
const sidebarVariants: Variants = {
hidden: { opacity: 0, x: -12 },
visible: {
opacity: 1,
x: 0,
transition: {
x: { duration: 0.25, ease: EASE_OUT },
opacity: { duration: 0.25, ease: EASE_OUT },
},
},
}
const viewTransition = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: 0.2, ease: EASE_OUT },
} as const
interface DemoStep {
type: 'workflow' | 'home' | 'logs'
workflowId?: string
duration: number
}
const WORKFLOW_MAP = new Map(PREVIEW_WORKFLOWS.map((w) => [w.id, w]))
const HOME_STEP_MS = 12000
const LOGS_STEP_MS = 5000
const DESKTOP_QUERY = '(min-width: 1024px)'
/** SSR-safe desktop media-query subscription for {@link useSyncExternalStore}. */
function subscribeDesktop(onChange: () => void) {
const mql = window.matchMedia(DESKTOP_QUERY)
mql.addEventListener('change', onChange)
return () => mql.removeEventListener('change', onChange)
}
const getDesktopSnapshot = () => window.matchMedia(DESKTOP_QUERY).matches
const getDesktopServerSnapshot = () => true
/** Full desktop sequence: CRM -> home -> logs -> ITSM -> support -> repeat */
const DESKTOP_STEPS: DemoStep[] = [
{
type: 'workflow',
workflowId: 'wf-self-healing-crm',
duration: getWorkflowStepDuration(WORKFLOW_MAP.get('wf-self-healing-crm')!),
},
{ type: 'home', duration: HOME_STEP_MS },
{ type: 'logs', duration: LOGS_STEP_MS },
{
type: 'workflow',
workflowId: 'wf-it-service',
duration: getWorkflowStepDuration(WORKFLOW_MAP.get('wf-it-service')!),
},
{
type: 'workflow',
workflowId: 'wf-customer-support',
duration: getWorkflowStepDuration(WORKFLOW_MAP.get('wf-customer-support')!),
},
]
interface LandingPreviewProps {
/**
* When false, render a static snapshot: no auto-cycle, no entrance/data-flow
* animation. Used when the preview is a faded backdrop behind an elevated
* callout rather than the live demo.
*/
autoplay?: boolean
/**
* Initial staged view for the static snapshot (`autoplay={false}`). Defaults
* to `'workflow'`. Lets each feature stage show the platform surface that
* matches its callout (e.g. `'logs'`, `'scheduled-tasks'`).
*/
initialView?: SidebarView
/** Initial workflow for the static snapshot. Defaults to the first preview workflow. */
initialWorkflowId?: string
}
/**
* Interactive workspace preview for the hero section.
*
* Desktop: auto-cycles CRM -> home -> logs -> ITSM -> support -> repeat.
* Mobile: static workflow canvas (no animation, no cycling).
* User interaction permanently stops the auto-cycle.
*
* Pass `autoplay={false}` to render a fully static snapshot (no cycling, no
* animation) - for use as a background behind a feature callout.
*/
export function LandingPreview({
autoplay = true,
initialView = 'workflow',
initialWorkflowId = PREVIEW_WORKFLOWS[0].id,
}: LandingPreviewProps) {
const [activeView, setActiveView] = useState<SidebarView>(initialView)
const [activeWorkflowId, setActiveWorkflowId] = useState(initialWorkflowId)
const [animationKey, setAnimationKey] = useState(0)
const [autoTypeHome, setAutoTypeHome] = useState(false)
const isDesktop = useSyncExternalStore(
subscribeDesktop,
getDesktopSnapshot,
getDesktopServerSnapshot
)
const demoIndexRef = useRef(0)
const demoTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const autoCycleActiveRef = useRef(true)
const clearDemoTimer = useCallback(() => {
if (demoTimerRef.current) {
clearTimeout(demoTimerRef.current)
demoTimerRef.current = null
}
}, [])
const applyDemoStep = useCallback((step: DemoStep) => {
setAutoTypeHome(false)
if (step.type === 'workflow' && step.workflowId) {
setActiveWorkflowId(step.workflowId)
setActiveView('workflow')
setAnimationKey((k) => k + 1)
} else if (step.type === 'home') {
setActiveView('home')
setAutoTypeHome(true)
} else if (step.type === 'logs') {
setActiveView('logs')
}
}, [])
const scheduleNextStep = useCallback(() => {
if (!autoCycleActiveRef.current) return
const steps = DESKTOP_STEPS
const currentStep = steps[demoIndexRef.current]
demoTimerRef.current = setTimeout(() => {
if (!autoCycleActiveRef.current) return
demoIndexRef.current = (demoIndexRef.current + 1) % steps.length
applyDemoStep(steps[demoIndexRef.current])
scheduleNextStep()
}, currentStep.duration)
}, [applyDemoStep])
useEffect(() => {
/** `isDesktop` is reactive, so gate on the cycle still being active - a resize after interaction must not restart. */
if (!isDesktop || !autoplay || !autoCycleActiveRef.current) return
/** Reset the index so a restart replays from step 0 (index, applied step, and delay stay consistent). */
demoIndexRef.current = 0
applyDemoStep(DESKTOP_STEPS[0])
scheduleNextStep()
return clearDemoTimer
}, [isDesktop, autoplay, applyDemoStep, scheduleNextStep, clearDemoTimer])
const stopAutoCycle = useCallback(() => {
autoCycleActiveRef.current = false
clearDemoTimer()
}, [clearDemoTimer])
const handleSelectWorkflow = useCallback(
(id: string) => {
stopAutoCycle()
setAutoTypeHome(false)
setActiveWorkflowId(id)
setActiveView('workflow')
setAnimationKey((k) => k + 1)
},
[stopAutoCycle]
)
const handleSelectHome = useCallback(() => {
stopAutoCycle()
setAutoTypeHome(false)
setActiveView('home')
}, [stopAutoCycle])
const handleSelectNav = useCallback(
(id: SidebarView) => {
stopAutoCycle()
setAutoTypeHome(false)
setActiveView(id)
},
[stopAutoCycle]
)
const activeWorkflow =
PREVIEW_WORKFLOWS.find((w) => w.id === activeWorkflowId) ?? PREVIEW_WORKFLOWS[0]
const isWorkflowView = activeView === 'workflow'
const isHomeView = activeView === 'home'
const chatName = isWorkflowView ? activeWorkflow.name : (CHAT_TITLES[activeView] ?? 'New chat')
/** Desktop demo motion only runs when autoplaying; otherwise a static snapshot. */
const animated = isDesktop && autoplay
return (
<LazyMotion features={domAnimation}>
<m.div
className='flex aspect-[1116/615] w-full overflow-hidden rounded bg-[var(--surface-1)] antialiased'
initial={animated ? 'hidden' : false}
animate='visible'
variants={containerVariants}
>
<m.div className='hidden lg:flex' variants={sidebarVariants}>
<LandingPreviewSidebar
workflows={PREVIEW_WORKFLOWS}
activeWorkflowId={activeWorkflowId}
activeView={activeView}
onSelectWorkflow={handleSelectWorkflow}
onSelectHome={handleSelectHome}
onSelectNav={handleSelectNav}
/>
</m.div>
<div className='flex min-w-0 flex-1 flex-col py-2 pr-2 pl-2 lg:pl-0'>
<div className='flex flex-1 overflow-hidden rounded-[5px] border border-[var(--border-1)] bg-[var(--surface-2)]'>
{isHomeView ? (
/* Home: the chat IS the whole view - its empty "What should we get
done?" state, with no resource staged. */
<div className='relative flex min-w-0 flex-1 flex-col overflow-hidden'>
{animated ? (
<AnimatePresence mode='wait'>
<m.div
key={`home-${animationKey}`}
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewHome autoType={autoTypeHome} />
</m.div>
</AnimatePresence>
) : (
<LandingPreviewHome autoType={autoTypeHome} />
)}
</div>
) : (
/* Chat everywhere: the persistent Mothership chat pane on the left,
a single staged resource on the right. The chat pane stays
mounted as the staged resource crossfades - the chat is the
constant the work hangs off of. */
<>
<m.div className='hidden lg:flex' variants={sidebarVariants}>
<LandingPreviewChat
chat={isDesktop ? getViewChat(activeView, activeWorkflow) : null}
chatName={chatName}
animationKey={animationKey}
/>
</m.div>
<div className='relative flex min-w-0 flex-1 flex-col overflow-hidden border-[var(--border-1)] border-l'>
{isWorkflowView && <LandingPreviewStageHeader name={activeWorkflow.name} />}
<div className='relative min-h-0 flex-1 overflow-hidden'>
{animated ? (
<AnimatePresence mode='wait'>
{activeView === 'workflow' && (
<m.div
key={`wf-${activeWorkflow.id}-${animationKey}`}
className='h-full w-full'
{...viewTransition}
>
<LandingPreviewWorkflow workflow={activeWorkflow} animate />
</m.div>
)}
{activeView === 'tables' && (
<m.div
key={`tables-${animationKey}`}
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewTables />
</m.div>
)}
{activeView === 'files' && (
<m.div
key='files'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewFiles />
</m.div>
)}
{activeView === 'knowledge' && (
<m.div
key='knowledge'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewKnowledge />
</m.div>
)}
{activeView === 'logs' && (
<m.div key='logs' className='flex h-full w-full flex-col' initial={false}>
<LandingPreviewLogs />
</m.div>
)}
{activeView === 'scheduled-tasks' && (
<m.div
key='scheduled-tasks'
className='flex h-full w-full flex-col'
{...viewTransition}
>
<LandingPreviewScheduledTasks />
</m.div>
)}
</AnimatePresence>
) : activeView === 'tables' ? (
<LandingPreviewTables />
) : activeView === 'files' ? (
<LandingPreviewFiles />
) : activeView === 'knowledge' ? (
<LandingPreviewKnowledge />
) : activeView === 'logs' ? (
<LandingPreviewLogs />
) : activeView === 'scheduled-tasks' ? (
<LandingPreviewScheduledTasks />
) : (
<LandingPreviewWorkflow workflow={activeWorkflow} />
)}
</div>
</div>
</>
)}
</div>
</div>
</m.div>
</LazyMotion>
)
}