Skip to content
Open
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
6 changes: 5 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"permissions": {
"defaultMode": "auto"
}
}
51 changes: 39 additions & 12 deletions src/components/Code/FlowDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@
import { IconActivity, IconLightBulb, IconMessage, IconSparkles, IconPullRequest } from '@posthog/icons'
import { usePrefersReducedMotion } from './usePrefersReducedMotion'

const steps = [
{ label: '1. Analyze\nusage', icon: IconActivity, actor: 'Human' as const },
{ label: '2. Decide what\nto build', icon: IconLightBulb, actor: 'Human' as const },
{ label: '3. Prompt &\ncontext', icon: IconMessage, actor: 'Human' as const },
{ label: '4. Build', icon: IconSparkles, actor: 'Machine' as const },
{ label: '5. Ship', icon: IconPullRequest, actor: 'Human' as const },
export interface FlowStep {
/** Step label. `\n` is preserved as a soft break in the grid layout and flattened to a space in the list layout. */
label: string
/** Optional secondary text shown after the label in the list layout (muted). */
description?: string
icon: React.ComponentType<{ className?: string }>
actor: 'Human' | 'Machine'
}

const defaultSteps: FlowStep[] = [
{ label: '1. Analyze\nusage', icon: IconActivity, actor: 'Human' },
{ label: '2. Decide what\nto build', icon: IconLightBulb, actor: 'Human' },
{ label: '3. Prompt &\ncontext', icon: IconMessage, actor: 'Human' },
{ label: '4. Build', icon: IconSparkles, actor: 'Machine' },
{ label: '5. Ship', icon: IconPullRequest, actor: 'Human' },
]

const actorColors: Record<string, string> = {
Expand All @@ -17,9 +26,20 @@

interface FlowDiagramProps {
className?: string
/** Ordered steps rendered in the card. Defaults to the "Coding with AI" flow. */
steps?: FlowStep[]
/** Left-aligned header label (uppercased via CSS). */
headerLeft?: string
/** Right-aligned header label (uppercased via CSS). */
headerRight?: string
}

export function FlowDiagram({ className = '' }: FlowDiagramProps) {
export function FlowDiagram({
className = '',
steps = defaultSteps,
headerLeft = 'Coding with AI',
headerRight = '(cir. 2022-2025)',
}: FlowDiagramProps) {
const ref = useRef<HTMLDivElement>(null)
const [isVisible, setIsVisible] = useState(false)
const prefersReducedMotion = usePrefersReducedMotion()
Expand Down Expand Up @@ -47,8 +67,8 @@
return (
<div ref={ref} className={`@container border border-primary rounded-sm overflow-hidden ${className}`}>
<div className="py-1.5 flex items-center justify-between border-b border-secondary mx-4 @xl:mx-5">
<span className="text-[13px] uppercase text-primary font-mono">Coding with AI</span>
<span className="text-[13px] uppercase text-primary font-mono">(cir. 2022-2025)</span>
<span className="text-[13px] uppercase text-primary font-mono">{headerLeft}</span>
<span className="text-[13px] uppercase text-primary font-mono">{headerRight}</span>
</div>
{/* Mobile: stacked list */}
<div className="flex flex-col gap-2 p-4 ">
Expand All @@ -63,10 +83,17 @@
}}
>
<step.icon className="size-5 text-primary shrink-0" />
<span className="text-sm text-primary whitespace-pre-line leading-tight">
{step.label.replace('\n', ' ')}
<span className="text-sm leading-tight min-w-0">
<span className="text-primary font-medium whitespace-pre-line">
{step.label.replace('\n', ' ')}

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This replaces only the first occurrence of '\n'.
</span>
{step.description && <span className="text-secondary"> {step.description}</span>}
</span>
<span className={`text-[13px] font-medium uppercase ml-auto ${actorColors[step.actor]}`}>
<span
className={`text-[13px] font-medium uppercase ml-auto pl-2 shrink-0 ${
actorColors[step.actor]
}`}
>
{step.actor}
</span>
</div>
Expand Down
22 changes: 16 additions & 6 deletions src/components/Products/ReaderViewProduct/templates/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,26 @@ const Overview = ({ id, productData }: SectionComponentProps) => {
<CloudinaryImage
src={screenshots.home.src as `https://res.cloudinary.com/${string}`}
alt={screenshots.home.alt || name}
className="w-full"
className={`w-full${screenshots.home.srcDark ? ' dark:hidden' : ''}`}
imgClassName="h-auto rounded-lg transition-all duration-300"
/>
<div className="absolute bottom-0 -right-4">
{screenshots.home.srcDark && (
<CloudinaryImage
src={hogs.default.src as `https://res.cloudinary.com/${string}`}
alt={hogs.default.alt || name}
imgClassName="h-36 @2xl/reader-content:h-48 transition-all duration-300"
src={screenshots.home.srcDark as `https://res.cloudinary.com/${string}`}
alt={screenshots.home.alt || name}
className="w-full hidden dark:block"
imgClassName="h-auto rounded-lg transition-all duration-300"
/>
</div>
)}
{hogs?.default?.src && (
<div className="absolute bottom-0 -right-4">
<CloudinaryImage
src={hogs.default.src as `https://res.cloudinary.com/${string}`}
alt={hogs.default.alt || name}
imgClassName="h-36 @2xl/reader-content:h-48 transition-all duration-300"
/>
</div>
)}
</Glow>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ const PricingFooterCTA = ({ id, productData }: SectionComponentProps) => {
The hedgehog has been waiting this whole time.
</h2>
<p className="text-base text-primary/70 mb-8">
It's free to start. Not "free trial" free actually free. No card, no call, no "someone from
It's free to start. Not "free trial" free actually free. No card, no call, no "someone from
our team will be in touch." Just PostHog.
</p>
<div className="flex flex-wrap gap-3 items-center">
<OSButton variant="primary" asLink to="https://app.posthog.com/signup" size="lg">
Get started free
Get started free
</OSButton>
<OSButton variant="secondary" asLink to="/talk-to-a-human" size="lg">
Talk to a human
Expand Down
169 changes: 169 additions & 0 deletions src/components/ReplayVision/AIPromptsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useState } from 'react'
import CloudinaryImage from 'components/CloudinaryImage'
import { ToggleGroup } from 'components/RadixUI/ToggleGroup'
import { LabeledList } from 'components/Products/ReaderViewProduct/helpers'
import type { SectionComponentProps } from 'components/Products/ReaderViewProduct/types'

const INTRO =
'Ask your AI agent to author scanners, scan a session, or read what Replay Vision found – without leaving your editor. Works in any MCP client via the PostHog MCP: Cursor, Claude Code, Codex, VS Code, and more.'

// Detective hedgehog surveilling a wall of monitors – sits where Session
// Replay's "AI prompts" section floats its hog.
const IMAGE =
'https://res.cloudinary.com/dmukukwp6/image/upload/noir_desk_relax_surveillance_computer_63a434c398.png' as const

interface PromptGroup {
title: string
tools: string[]
prompts: string[]
}

const promptGroups: PromptGroup[] = [
{
title: 'Create a scanner',
tools: ['vision-scanners-create'],
prompts: [
'Create a Replay Vision scanner that flags sessions where users get stuck on the /checkout page, narrow to enterprise customers, and start at 25% sampling. Estimate the cost first.',
'Set up a summarizer scanner for my onboarding flow.',
'Make a scanner that scores how frustrated users are on the /pricing page.',
],
},
{
title: 'Size it before you commit',
tools: ['vision-scanners-estimate-create', 'vision-quota-retrieve'],
prompts: [
'Estimate how many observations that scanner would produce this month before creating it.',
'How much Replay Vision quota do we have left this month?',
],
},
{
title: 'Scan a session on demand',
tools: ['vision-scanners-scan-session'],
prompts: [
"Scan session abc123 with the 'Dead-end pages' scanner and tell me what it found, including the reasoning.",
],
},
{
title: 'Read observations',
tools: ['vision-observations-list', 'vision-scanners-observations-list'],
prompts: [
'Find every Replay Vision observation for session abc123 and give me a one-line summary of each.',
"List the last 20 observations from my 'Frustration score' scanner and summarize what's driving high scores.",
],
},
{
title: 'Tune a scanner',
tools: ['vision-scanners-list', 'vision-scanners-update'],
prompts: [
'List all my scanners and show me the config for the frustration one.',
"Bump the 'Dead-end pages' scanner to 50% sampling.",
],
},
]

const toolGroups: { job: string; tools: string[] }[] = [
{
job: 'Author scanners',
tools: [
'vision-scanners-list',
'vision-scanners-get',
'vision-scanners-create',
'vision-scanners-update',
'vision-scanners-delete',
],
},
{
job: 'Size before creating',
tools: ['vision-scanners-estimate-create', 'vision-quota-retrieve'],
},
{
job: 'Scan on demand',
tools: ['vision-scanners-scan-session'],
},
{
job: 'Read observations',
tools: [
'vision-observations-list',
'vision-observations-retrieve',
'vision-scanners-observations-list',
'vision-scanners-observations-get',
],
},
]

const AIPromptsSection = ({ id }: SectionComponentProps) => {
const [tab, setTab] = useState<'prompts' | 'tools'>('prompts')

return (
<section id={id} className="scroll-mt-20 not-prose">
<h2 className="mb-3">AI prompts</h2>
<p className="text-base text-secondary mb-4">{INTRO}</p>
<ToggleGroup
title="View"
hideTitle
value={tab}
onValueChange={(value) => {
if (value === 'prompts' || value === 'tools') setTab(value)
}}
options={[
{ label: 'Example prompts', value: 'prompts', default: true },
{ label: 'Tools', value: 'tools' },
]}
className="mb-4 max-w-sm"
/>
<div className="@container bg-primary rounded shadow-2xl p-4 @2xl/reader-content:p-8 @4xl/reader-content:p-10">
<div className="flex-1 min-w-0 w-full">
{tab === 'prompts' && (
<>
<aside className="w-48 mx-auto mb-4 @lg:ml-4 @lg:mr-0 @lg:float-right @lg:w-32 @xl:w-48 @2xl:w-80 @6xl:w-96 @xl:ml-8 transition-all">
<CloudinaryImage
src={IMAGE}
alt="Replay Vision watching your sessions"
className="w-full"
/>
</aside>
<div className="space-y-6">
{promptGroups.map((g) => (
<div key={g.title}>
<h3 className="text-base mb-3 flex flex-wrap items-baseline gap-x-2 border-b border-primary pb-1">
<span>{g.title}</span>
<span className="text-xs font-mono font-normal text-secondary">
{g.tools.join(' · ')}
</span>
</h3>
<ul className="list-none pl-0 m-0 space-y-1 text-sm text-secondary italic leading-relaxed">
{g.prompts.map((p) => (
<li key={p}>&ldquo;{p}&rdquo;</li>
))}
</ul>
</div>
))}
</div>
</>
)}

{tab === 'tools' && (
<>
<p className="text-sm text-secondary mb-4">
All Replay Vision MCP tools are prefixed{' '}
<code className="font-mono text-[13px]">vision-</code>:
</p>
<div className="@container">
<LabeledList
items={toolGroups.map((g) => ({
label: g.job,
description: (
<span className="font-mono text-[13px]">{g.tools.join(' · ')}</span>
),
}))}
/>
</div>
</>
)}
</div>
</div>
</section>
)
}

export default AIPromptsSection
36 changes: 36 additions & 0 deletions src/components/ReplayVision/HowToUseSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react'
import TabbedCarousel from 'components/TabbedCarousel'
import CarouselSlide from 'components/Products/ReaderViewProduct/CarouselSlide'
import type { SectionComponentProps } from 'components/Products/ReaderViewProduct/types'
import { applications } from 'hooks/productData/replay_vision/slides'

// "How do I use it?" – renders the applications carousel in the shared
// TabbedCarousel format (same primitives as the Applications template), with
// Replay Vision's own heading and intro copy.
const HowToUseSection = ({ id, productData }: SectionComponentProps) => {
if (!applications.length) return null

return (
<section id={id} className="scroll-mt-20 not-prose">
<h2 className="mb-8">How do I use it?</h2>
<p>There are a few ways to put Replay Vision to work.</p>
<TabbedCarousel
tabs={applications.map((s) => ({
value: s.slug,
label: s.label,
icon: s.icon,
color: s.color,
activeText: s.activeText,
progressBar: s.progressBar,
content: <CarouselSlide slide={s} productData={productData} />,
}))}
slideDuration={6000}
showActiveBg={false}
slideClassName="!min-h-0 !p-0 !rounded"
className="mt-4 mb-12"
/>
</section>
)
}

export default HowToUseSection
Loading
Loading