Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added apps/website/public/screenshots/cockpit-api.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/website/public/screenshots/cockpit-code.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/website/public/screenshots/cockpit-docs.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/website/public/screenshots/cockpit-run.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
163 changes: 163 additions & 0 deletions apps/website/scripts/capture-screenshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Capture product screenshots from the live cockpit demo
* for use in the marketing site's BrowserFrame placeholders.
*
* Captures cockpit.cacheplane.ai in each of its 4 modes (Run, Code,
* Docs, API) at 2× DPR, then crops the cockpit content well, optimizes
* to WebP, and writes to apps/website/public/screenshots/.
*
* Usage:
* pnpm tsx apps/website/scripts/capture-screenshots.ts
*
* Optional flags:
* --url <url> Override the cockpit URL (default cockpit.cacheplane.ai)
* --keep-png Keep the intermediate PNG files (for debugging)
*
* The script is idempotent — it overwrites existing files in
* apps/website/public/screenshots/. The output WebP files are committed
* to the repo so the marketing site can use them at build time without
* needing this script to run.
*/
import { chromium, type Page } from 'playwright';
import sharp from 'sharp';
import { mkdir, writeFile, unlink } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { existsSync } from 'node:fs';

const DEFAULT_COCKPIT_URL =
'https://cockpit.cacheplane.ai/langgraph/core-capabilities/streaming/overview/python';

interface CaptureTarget {
/** Output filename (without extension). */
name: string;
/** Cockpit mode to switch to before capturing. */
mode: 'Run' | 'Code' | 'Docs' | 'API';
/**
* Selector for the element to capture. If omitted, captures the cockpit
* content section (everything except the sidebar) at full size.
*/
selector?: string;
/** Additional wait after mode click before screenshotting (ms). */
settleMs?: number;
}

const TARGETS: CaptureTarget[] = [
// Hero collage back frame + Stream FeatureBlock + Pilot "Build" block.
// The "Run" mode shows the live chat surface — captures real product UI.
{ name: 'cockpit-run', mode: 'Run', settleMs: 4000 },
// Hero collage front frame replacement — Code mode shows the agent
// source code in a tabbed code panel.
{ name: 'cockpit-code', mode: 'Code', settleMs: 1500 },
// Render FeatureBlock visual — Docs mode shows narrative documentation
// (well-structured content, looks like rendered output).
{ name: 'cockpit-docs', mode: 'Docs', settleMs: 1500 },
// API mode shows the API reference renderer — useful alternative
// for the Render block visual.
{ name: 'cockpit-api', mode: 'API', settleMs: 1500 },
];

interface Args {
url: string;
keepPng: boolean;
}

function parseArgs(): Args {
const args = process.argv.slice(2);
const out: Args = { url: DEFAULT_COCKPIT_URL, keepPng: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--url' && i + 1 < args.length) {
out.url = args[++i];
} else if (args[i] === '--keep-png') {
out.keepPng = true;
}
}
return out;
}

async function ensureDir(path: string): Promise<void> {
if (!existsSync(path)) await mkdir(path, { recursive: true });
}

async function switchMode(page: Page, mode: CaptureTarget['mode']): Promise<void> {
// Cockpit's ModeSwitcher renders buttons with the mode name as text.
// Target the exact button by accessible name.
const button = page.getByRole('button', { name: mode, exact: true });
await button.waitFor({ state: 'visible', timeout: 10_000 });
await button.click();
}

async function captureOne(
page: Page,
target: CaptureTarget,
outputDir: string,
keepPng: boolean,
): Promise<{ png: string; webp: string }> {
console.log(` → switching to ${target.mode} mode`);
await switchMode(page, target.mode);

const settle = target.settleMs ?? 1500;
console.log(` → waiting ${settle}ms for content to settle`);
await page.waitForTimeout(settle);

// Capture the cockpit content section (not the sidebar — we want the
// mode content visible at the top, not the sidebar nav).
const locator = target.selector
? page.locator(target.selector)
: page.locator('main[aria-label="Cockpit shell"] section').first();

const pngPath = join(outputDir, `${target.name}.png`);
const webpPath = join(outputDir, `${target.name}.webp`);

console.log(` → screenshotting → ${target.name}.png`);
await locator.screenshot({ path: pngPath, type: 'png' });

console.log(` → optimizing → ${target.name}.webp`);
await sharp(pngPath)
.resize({ width: 1400, withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(webpPath);

if (!keepPng) await unlink(pngPath);

return { png: pngPath, webp: webpPath };
}

async function main(): Promise<void> {
const args = parseArgs();
const outputDir = join(process.cwd(), 'apps/website/public/screenshots');
await ensureDir(outputDir);

console.log(`Capturing cockpit screenshots from: ${args.url}`);
console.log(`Output: ${outputDir}\n`);

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 2,
});
const page = await context.newPage();

try {
console.log(`Loading cockpit at ${args.url}`);
await page.goto(args.url, { waitUntil: 'networkidle', timeout: 30_000 });

// Wait for cockpit shell to hydrate.
await page.waitForSelector('[data-hydrated="true"]', { timeout: 15_000 });
console.log('Cockpit hydrated ✓\n');

for (const target of TARGETS) {
console.log(`Capturing: ${target.name}`);
await captureOne(page, target, outputDir, args.keepPng);
console.log('');
}

console.log('✓ All screenshots captured.');
} finally {
await browser.close();
}
}

main().catch((err) => {
console.error('Capture failed:', err);
process.exit(1);
});
131 changes: 16 additions & 115 deletions apps/website/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,58 +41,14 @@ export default async function HomePage() {
]}
cta={{ label: 'Read the streaming guide', href: '/docs/agent/api/agent' }}
visual={
<BrowserFrame url="cockpit.cacheplane.ai/chat" elevation="md">
<div
style={{
padding: 28,
background: 'linear-gradient(180deg, #fff 0%, #f8fafc 100%)',
minHeight: 320,
}}
>
<div
style={{
fontFamily: tokens.typography.fontMono,
fontSize: 11,
color: tokens.colors.textMuted,
marginBottom: 12,
}}
>
ASSISTANT · streaming
</div>
<div
style={{
fontFamily: tokens.typography.fontSans,
fontSize: 14,
color: tokens.colors.textPrimary,
lineHeight: 1.6,
}}
>
Building the Angular chat surface from your existing component library. Tool call results render as Angular components, not raw JSON.
<span
style={{
display: 'inline-block',
width: 6,
height: 14,
background: tokens.colors.accent,
marginLeft: 2,
verticalAlign: 'middle',
animation: 'blink 1s steps(2) infinite',
}}
/>
</div>
<div
style={{
marginTop: 20,
paddingTop: 16,
borderTop: `1px solid ${tokens.surfaces.border}`,
fontFamily: tokens.typography.fontMono,
fontSize: 11,
color: tokens.colors.textSecondary,
}}
>
tools: render_card · search_docs · stream complete
</div>
</div>
<BrowserFrame url="cockpit.cacheplane.ai/langgraph/streaming" elevation="md">
<img
src="/screenshots/cockpit-docs.webp"
alt="Cockpit reference app — Angular streaming guide with provideAgent setup"
style={{ display: 'block', width: '100%', height: 'auto' }}
loading="lazy"
decoding="async"
/>
</BrowserFrame>
}
/>
Expand All @@ -117,69 +73,14 @@ export default async function HomePage() {
cta={{ label: 'See @ngaf/render', href: '/render' }}
visualLeft
visual={
<BrowserFrame url="cockpit.cacheplane.ai/render" elevation="md">
<div
style={{
padding: 28,
background: tokens.surfaces.surface,
minHeight: 320,
}}
>
<div
style={{
background: tokens.surfaces.surfaceTinted,
border: `1px solid ${tokens.surfaces.border}`,
borderRadius: tokens.radius.md,
padding: 16,
marginBottom: 12,
}}
>
<div
style={{
fontFamily: tokens.typography.fontMono,
fontSize: 11,
color: tokens.colors.accent,
marginBottom: 8,
}}
>
AI-rendered · Angular component
</div>
<div
style={{
fontFamily: tokens.typography.fontSerif,
fontSize: 18,
fontWeight: 700,
color: tokens.colors.textPrimary,
marginBottom: 6,
}}
>
Q3 revenue: $4.2M
</div>
<div
style={{
fontFamily: tokens.typography.fontSans,
fontSize: 13,
color: tokens.colors.textSecondary,
lineHeight: 1.5,
}}
>
+18% vs Q2. Driven by enterprise upsells in the EU.
</div>
</div>
<div
style={{
background: tokens.colors.accentSurface,
border: `1px dashed ${tokens.colors.accentBorder}`,
borderRadius: tokens.radius.sm,
padding: '8px 12px',
fontFamily: tokens.typography.fontMono,
fontSize: 11,
color: tokens.colors.accent,
}}
>
fallback ready · readiness gate ✓
</div>
</div>
<BrowserFrame url="cockpit.cacheplane.ai/langgraph/api" elevation="md">
<img
src="/screenshots/cockpit-api.webp"
alt="Cockpit reference app — API reference rendered as structured cards"
style={{ display: 'block', width: '100%', height: 'auto' }}
loading="lazy"
decoding="async"
/>
</BrowserFrame>
}
/>
Expand Down
20 changes: 8 additions & 12 deletions apps/website/src/app/pilot-to-prod/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,18 +132,14 @@ export default function PilotToProdPage() {
cta={{ label: 'See @ngaf/chat', href: '/chat' }}
visualLeft
visual={
<BrowserFrame url="cockpit.cacheplane.ai/chat" elevation="md">
<div style={{ padding: 28, minHeight: 320, background: 'linear-gradient(180deg, #fff, #f8fafc)' }}>
<div style={{ fontFamily: tokens.typography.fontMono, fontSize: 11, color: tokens.colors.textMuted, marginBottom: 12 }}>
ASSISTANT · streaming · your data
</div>
<div style={{ fontFamily: tokens.typography.fontSans, fontSize: 14, color: tokens.colors.textPrimary, lineHeight: 1.6 }}>
Reviewing the Q3 expense reports flagged for compliance. Three line items need approval before processing — pulled the audit trail for each.
</div>
<div style={{ marginTop: 20, paddingTop: 16, borderTop: `1px solid ${tokens.surfaces.border}`, fontFamily: tokens.typography.fontMono, fontSize: 11, color: tokens.colors.textSecondary }}>
tools: query_expenses · fetch_audit · request_approval (interrupt)
</div>
</div>
<BrowserFrame url="cockpit.cacheplane.ai" elevation="md">
<img
src="/screenshots/cockpit-run.webp"
alt="Cockpit reference app — live chat surface ready to receive a message"
style={{ display: 'block', width: '100%', height: 'auto' }}
loading="lazy"
decoding="async"
/>
</BrowserFrame>
}
/>
Expand Down
48 changes: 8 additions & 40 deletions apps/website/src/components/landing/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,50 +76,18 @@ export function Hero() {
{/* Right column — layered collage */}
<div style={{ position: 'relative', minHeight: 420 }} aria-hidden="true">
<BrowserFrame
url="cockpit.cacheplane.ai/chat"
url="cockpit.cacheplane.ai"
rotate={-3}
elevation="lg"
style={{ position: 'absolute', top: 0, left: 0, width: '92%' }}
>
<div
style={{
padding: 32,
background: 'linear-gradient(180deg, #fff 0%, #f4f6fb 100%)',
minHeight: 220,
}}
>
<div
style={{
fontFamily: tokens.typography.fontMono,
fontSize: 12,
color: tokens.colors.textMuted,
marginBottom: 8,
}}
>
AI · streaming
</div>
<div
style={{
fontFamily: tokens.typography.fontSans,
fontSize: 14,
color: tokens.colors.textPrimary,
lineHeight: 1.5,
}}
>
Generating production-ready Angular components from your schema…
<span
style={{
display: 'inline-block',
width: 6,
height: 14,
background: tokens.colors.accent,
marginLeft: 2,
verticalAlign: 'middle',
animation: 'blink 1s steps(2) infinite',
}}
/>
</div>
</div>
<img
src="/screenshots/cockpit-code.webp"
alt="Cockpit reference app showing the Angular streaming component source"
style={{ display: 'block', width: '100%', height: 'auto' }}
loading="lazy"
decoding="async"
/>
</BrowserFrame>
<BrowserFrame
url="agent.signal()"
Expand Down
Loading