From ccff942399c98bef90ffdedf0867db73669b1210 Mon Sep 17 00:00:00 2001 From: Anna Nguyen Date: Mon, 1 Jun 2026 17:08:45 -0400 Subject: [PATCH 1/3] Add interactive workbench demo for homepage hero. Introduces exclusiveDemo hero variation with an embeddable five-step tour, static workbench-demo iframe bundle, and scoped framing headers. Restores exclusive for other pages with full-bleed images. Co-authored-by: Cursor --- next.config.ts | 21 +- package.json | 5 +- prismicio-types.d.ts | 78 + public/workbench-demo/app.jsx | 160 ++ public/workbench-demo/cc-home-sections.jsx | 225 +++ public/workbench-demo/cc-home-shared.jsx | 100 ++ public/workbench-demo/cc-home.jsx | 75 + public/workbench-demo/cc-job.jsx | 611 +++++++ public/workbench-demo/cc-prompt.jsx | 109 ++ public/workbench-demo/cc-shell.jsx | 75 + public/workbench-demo/cc-workbench.jsx | 298 ++++ public/workbench-demo/demo-hints.jsx | 60 + public/workbench-demo/demo-tour.jsx | 168 ++ public/workbench-demo/ds-brand-icons.jsx | 173 ++ public/workbench-demo/ds-icons.jsx | 417 +++++ public/workbench-demo/ds-trace.jsx | 1047 ++++++++++++ public/workbench-demo/ds-viz.jsx | 1513 +++++++++++++++++ public/workbench-demo/index.html | 42 + public/workbench-demo/styles.css | 1197 +++++++++++++ public/workbench-demo/tour-config.js | 62 + scripts/sync-demo-icons.mjs | 220 +++ scripts/sync-workbench-tour-config.mjs | 36 + src/components/ui/Button.tsx | 2 +- src/components/workbench-demo/README.md | 35 + .../workbench-demo/WorkbenchDemoFrame.tsx | 265 +++ .../workbench-demo/bookDemoTarget.ts | 11 + .../workbench-demo/workbenchDemoTour.json | 60 + .../workbench-demo/workbenchDemoTour.ts | 65 + src/slices/main/Contact/ContactDefault.tsx | 6 +- src/slices/main/Hero/HeroExclusiveDemo.tsx | 64 + src/slices/main/Hero/index.tsx | 1 + src/slices/main/Hero/mocks.json | 61 + src/slices/main/Hero/model.json | 64 + 33 files changed, 7318 insertions(+), 8 deletions(-) create mode 100644 public/workbench-demo/app.jsx create mode 100644 public/workbench-demo/cc-home-sections.jsx create mode 100644 public/workbench-demo/cc-home-shared.jsx create mode 100644 public/workbench-demo/cc-home.jsx create mode 100644 public/workbench-demo/cc-job.jsx create mode 100644 public/workbench-demo/cc-prompt.jsx create mode 100644 public/workbench-demo/cc-shell.jsx create mode 100644 public/workbench-demo/cc-workbench.jsx create mode 100644 public/workbench-demo/demo-hints.jsx create mode 100644 public/workbench-demo/demo-tour.jsx create mode 100644 public/workbench-demo/ds-brand-icons.jsx create mode 100644 public/workbench-demo/ds-icons.jsx create mode 100644 public/workbench-demo/ds-trace.jsx create mode 100644 public/workbench-demo/ds-viz.jsx create mode 100644 public/workbench-demo/index.html create mode 100644 public/workbench-demo/styles.css create mode 100644 public/workbench-demo/tour-config.js create mode 100644 scripts/sync-demo-icons.mjs create mode 100644 scripts/sync-workbench-tour-config.mjs create mode 100644 src/components/workbench-demo/README.md create mode 100644 src/components/workbench-demo/WorkbenchDemoFrame.tsx create mode 100644 src/components/workbench-demo/bookDemoTarget.ts create mode 100644 src/components/workbench-demo/workbenchDemoTour.json create mode 100644 src/components/workbench-demo/workbenchDemoTour.ts create mode 100644 src/slices/main/Hero/HeroExclusiveDemo.tsx diff --git a/next.config.ts b/next.config.ts index ea3cf9ff..1c8f4683 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,14 +5,27 @@ const withNextIntl = createNextIntlPlugin() const nextConfig: NextConfig = { async headers() { + const sharedSecurityHeaders = [ + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, + ] + const frameDenyHeaders = [ + { key: 'X-Frame-Options', value: 'DENY' }, + { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" }, + ] + return [ { source: '/:path*', + headers: [...sharedSecurityHeaders, ...frameDenyHeaders], + }, + // Allow embedding the interactive workbench demo on our own pages. + { + source: '/workbench-demo/:path*', headers: [ - { key: 'X-Frame-Options', value: 'DENY' }, - { key: 'X-Content-Type-Options', value: 'nosniff' }, - { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, - { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" }, + ...sharedSecurityHeaders, + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + { key: 'Content-Security-Policy', value: "frame-ancestors 'self'" }, ], }, ] diff --git a/package.json b/package.json index dd469c7b..68726730 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,13 @@ }, "scripts": { "dev": "concurrently \"npm:dev:next\" \"npm:dev:slicemachine\" --names \"next,slicemachine\" --prefix-colors blue,magenta", - "dev:next": "next dev -p 3004", + "dev:next": "npm run sync:workbench-tour && next dev -p 3004", "dev:slicemachine": "start-slicemachine", - "build": "next build", + "build": "npm run sync:workbench-tour && next build", "start": "next start", "lint": "next lint", "lint:fix": "next lint --fix", + "sync:workbench-tour": "node scripts/sync-workbench-tour-config.mjs", "prepare": "husky" }, "dependencies": { diff --git a/prismicio-types.d.ts b/prismicio-types.d.ts index fc62cb4d..8eb1d89c 100644 --- a/prismicio-types.d.ts +++ b/prismicio-types.d.ts @@ -2449,6 +2449,81 @@ export type HeroSliceExclusive = prismic.SharedSliceVariation< never > +/** + * Primary content in *Hero → Exclusive (interactive demo) → Primary* + */ +export interface HeroSliceExclusiveDemoPrimary { + /** + * Eyebrow field in *Hero → Exclusive (interactive demo) → Primary* + * + * - **Field Type**: Link + * - **Placeholder**: *None* + * - **API ID Path**: hero.exclusiveDemo.primary.eyebrow + * - **Documentation**: https://prismic.io/docs/fields/link + */ + eyebrow: prismic.LinkField + + /** + * Title field in *Hero → Exclusive (interactive demo) → Primary* + * + * - **Field Type**: Rich Text + * - **Placeholder**: *None* + * - **API ID Path**: hero.exclusiveDemo.primary.title + * - **Documentation**: https://prismic.io/docs/fields/rich-text + */ + title: prismic.RichTextField + + /** + * Description field in *Hero → Exclusive (interactive demo) → Primary* + * + * - **Field Type**: Rich Text + * - **Placeholder**: *None* + * - **API ID Path**: hero.exclusiveDemo.primary.description + * - **Documentation**: https://prismic.io/docs/fields/rich-text + */ + description: prismic.RichTextField + + /** + * CTA field in *Hero → Exclusive (interactive demo) → Primary* + * + * - **Field Type**: Link + * - **Placeholder**: *None* + * - **API ID Path**: hero.exclusiveDemo.primary.cta + * - **Documentation**: https://prismic.io/docs/fields/link + */ + cta: prismic.LinkField< + string, + string, + unknown, + prismic.FieldState, + 'Primary' | 'Secondary' + > + + /** + * Eyebrow Emphasized field in *Hero → Exclusive (interactive demo) → Primary* + * + * - **Field Type**: Boolean + * - **Placeholder**: *None* + * - **Default Value**: true + * - **API ID Path**: hero.exclusiveDemo.primary.eyebrow_emphasized + * - **Documentation**: https://prismic.io/docs/fields/boolean + */ + eyebrow_emphasized: prismic.BooleanField +} + +/** + * Exclusive (interactive demo) variation for Hero Slice + * + * - **API ID**: `exclusiveDemo` + * - **Description**: Homepage hero with embedded workbench walkthrough + * - **Documentation**: https://prismic.io/docs/slices + */ +export type HeroSliceExclusiveDemo = prismic.SharedSliceVariation< + 'exclusiveDemo', + Simplify, + never +> + /** * Primary content in *Hero → Video → Primary* */ @@ -2782,6 +2857,7 @@ export type HeroSliceExclusiveAnimated = prismic.SharedSliceVariation< */ type HeroSliceVariation = | HeroSliceExclusive + | HeroSliceExclusiveDemo | HeroSliceVideo | HeroSliceDefault | HeroSliceImage @@ -5068,12 +5144,14 @@ declare module '@prismicio/client' { FeaturesSliceHorizontallyScrollable, HeroSlice, HeroSliceExclusivePrimary, + HeroSliceExclusiveDemoPrimary, HeroSliceVideoPrimary, HeroSliceDefaultPrimary, HeroSliceImagePrimary, HeroSliceExclusiveAnimatedPrimary, HeroSliceVariation, HeroSliceExclusive, + HeroSliceExclusiveDemo, HeroSliceVideo, HeroSliceDefault, HeroSliceImage, diff --git a/public/workbench-demo/app.jsx b/public/workbench-demo/app.jsx new file mode 100644 index 00000000..b2858057 --- /dev/null +++ b/public/workbench-demo/app.jsx @@ -0,0 +1,160 @@ +/* Marketing embed — workbench walkthrough (home → prompt → alerts → job). */ + +const App = () => { + const [screen, setScreen] = React.useState('home'); + const [promptKey, setPromptKey] = React.useState(0); + const [jobKey, setJobKey] = React.useState(0); + const [jobPhase, setJobPhase] = React.useState('running'); + const [promptReady, setPromptReady] = React.useState(false); + const screenRef = React.useRef(screen); + screenRef.current = screen; + + const tour = React.useMemo( + () => window.getTourState(screen, promptReady, jobPhase), + [screen, promptReady, jobPhase] + ); + + React.useEffect(() => { + const r = document.documentElement; + r.style.setProperty('--purple-400', '#4A51F2'); + r.style.setProperty('--purple-350', '#5D63F4'); + r.style.setProperty('--purple-300', '#747AF6'); + r.style.setProperty('--purple-200', '#9FA3F9'); + r.style.setProperty('--action-primary', '#4A51F2'); + r.style.setProperty('--action-primary-hover', '#5D63F4'); + r.style.setProperty('--density', '1'); + r.removeAttribute('data-theme'); + }, []); + + const postStep = React.useCallback(() => { + const t = window.getTourState(screen, promptReady, jobPhase); + const step = t?.index ?? 0; + try { + window.parent.postMessage( + { + type: 'workbench-demo-step', + step, + total: t?.total ?? window.TOUR_TOTAL, + title: t?.title ?? null, + barVerb: t?.barVerb ?? null, + barRest: t?.barRest ?? null, + srCaption: t?.caption ?? null, + passive: !!t?.passive, + substep: screen === 'job' ? jobPhase : null, + }, + window.location.origin + ); + } catch (_) {} + }, [screen, promptReady, jobPhase]); + + React.useEffect(() => { + postStep(); + }, [postStep]); + + const resetPromptScreen = React.useCallback(() => { + setPromptReady(false); + setPromptKey((k) => k + 1); + }, []); + + const openPrompt = React.useCallback( + (opts) => { + const reset = opts?.reset !== false; + if (reset) resetPromptScreen(); + setScreen('prompt'); + }, + [resetPromptScreen] + ); + + const handlePromptReady = React.useCallback(() => { + setPromptReady(true); + }, []); + + const openWorkbench = () => setScreen('workbench'); + + const viewJob = (done = false) => { + const settled = done === true; + setJobPhase(settled ? 'done' : 'running'); + setJobKey((k) => k + 1); + setScreen('job'); + }; + + const backToAlerts = () => setScreen('workbench'); + + const goHome = () => { + setPromptReady(false); + setScreen('home'); + }; + + const goToTourStep = React.useCallback( + (step) => { + switch (step) { + case 0: + goHome(); + break; + case 1: + openPrompt({ reset: screenRef.current !== 'prompt' }); + break; + case 2: + setScreen('workbench'); + break; + case 3: + viewJob(false); + break; + case 4: + viewJob(true); + break; + default: + break; + } + }, + [openPrompt] + ); + + React.useEffect(() => { + const onMessage = (event) => { + if (event.origin !== window.location.origin) return; + if (event.source !== window.parent) return; + if (event.data?.type !== 'workbench-demo-advance') return; + const step = Number(event.data.step); + if (Number.isFinite(step) && step >= 0 && step < window.TOUR_TOTAL) { + goToTourStep(step); + } + }; + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); + }, [goToTourStep]); + + return ( + +
+ + {screen === 'home' && ( +
+ +
+ )} + {screen === 'prompt' && ( +
+ +
+ )} + {screen === 'workbench' && } + {screen === 'job' && ( + + )} +
+
+ ); +}; + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/public/workbench-demo/cc-home-sections.jsx b/public/workbench-demo/cc-home-sections.jsx new file mode 100644 index 00000000..5176eed4 --- /dev/null +++ b/public/workbench-demo/cc-home-sections.jsx @@ -0,0 +1,225 @@ +/* Shared Recent jobs + Your workbenches blocks (home and prompt screens). */ + +const BrandMarks = ({ names }) => ( + <> + {names.map((name) => )} + +); + +const RecentJobCard = ({ time, workbench, children, onClick }) => ( + +); + +const InlineChip = ({ icon, children }) => ( + + {icon} + {children} + +); + +const WorkbenchCard = ({ name, desc, agent, agentIcon, webhooks, tools, onClick }) => ( + +); + +const SectionTitle = ({ children }) => ( +

+ {children} +

+); + +const CCHomeSections = ({ onPreviewPrompt, onOpenWorkbench }) => ( +
+
+ Recent jobs +
+ + Search for anything broken across alerting and infrastructure state, and create up to 3 prs to fix the… + + + }>EKS Cost Estimation + generate a full cost report of all eks clusters for me, in particular call out… + + + Do an analysis of the{' '} + }>elastic-stack (mgmt){' '} + elasticsearch relevant metrics an… + +
+
+ +
+ Your workbenches +
+ } + webhooks={} + tools={} + onClick={onOpenWorkbench} + /> + } + webhooks={} + tools={ + <> + + +3 + + } + onClick={onPreviewPrompt(true)} + /> + } + webhooks={} + tools={} + onClick={onOpenWorkbench} + /> +
+
+
+); + +Object.assign(window, { CCHomeSections }); diff --git a/public/workbench-demo/cc-home-shared.jsx b/public/workbench-demo/cc-home-shared.jsx new file mode 100644 index 00000000..962f46fe --- /dev/null +++ b/public/workbench-demo/cc-home-shared.jsx @@ -0,0 +1,100 @@ +/* Shared workbench home chrome — hero + prompt card shell. */ + +const PROMPT_CARD_STYLE = { + background: 'var(--fill-one)', + border: '1px solid var(--border-input)', + borderRadius: 'var(--r-large)', + padding: 'var(--sp-medium)', + display: 'flex', + flexDirection: 'column', + gap: 'var(--sp-medium)', + marginBottom: 56, +}; + +const PROMPT_TEXTAREA_STYLE = { + border: 'none', + outline: 'none', + resize: 'none', + background: 'transparent', + color: 'var(--text)', + fontFamily: 'var(--font-sans)', + fontSize: 15, + lineHeight: '24px', + letterSpacing: '0.5px', + minHeight: 56, + width: '100%', +}; + +const WorkbenchHero = () => ( +
+ + + +

+ What can I do for you today, Michael? +

+
+); + +const WorkbenchPillButton = ({ label, onClick, withHint }) => { + const pill = ( + + ); + if (!withHint) return pill; + return ( + + + {pill} + + + ); +}; + +Object.assign(window, { + PROMPT_CARD_STYLE, + PROMPT_TEXTAREA_STYLE, + WorkbenchHero, + WorkbenchPillButton, +}); diff --git a/public/workbench-demo/cc-home.jsx b/public/workbench-demo/cc-home.jsx new file mode 100644 index 00000000..f2c44e6f --- /dev/null +++ b/public/workbench-demo/cc-home.jsx @@ -0,0 +1,75 @@ +/* cc-home.jsx — Workbenches home: hero prompt + recent jobs + your workbenches. */ + +const CCHome = ({ onComposePrompt, onOpenWorkbench }) => { + const [wb, setWb] = React.useState('Select workbench'); + const [val, setVal] = React.useState(''); + const pickWorkbench = () => { + setWb('demo-workbench'); + onComposePrompt(); + }; + + const handlePromptIntent = () => { + pickWorkbench(); + }; + + const previewPrompt = (instant) => () => { + if (instant) onComposePrompt(); + }; + return ( +
+
+ + + +
+