Skip to content

Commit ba889b3

Browse files
committed
feat: posthog code
1 parent 201d88a commit ba889b3

File tree

12 files changed

+938
-2
lines changed

12 files changed

+938
-2
lines changed

src/components/Desktop/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ export const useProductLinks = () => {
8585
onClick: () => openNewChat({ path: `ask-max` }),
8686
source: 'desktop',
8787
},
88+
...(posthog?.isFeatureEnabled?.('posthog-code-website')
89+
? [
90+
{
91+
label: 'PostHog Code',
92+
Icon: <AppIcon name="script" />,
93+
url: '/code',
94+
source: 'desktop',
95+
},
96+
]
97+
: []),
8898
...(posthogInstance
8999
? [
90100
{
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { useRef, useEffect } from 'react'
2+
import { IconDocument, IconSearch, IconTerminal, IconBrain, IconCode } from '@posthog/icons'
3+
import { ConversationItem } from './types'
4+
5+
const toolIcons: Record<string, React.ComponentType<{ className?: string }>> = {
6+
Read: IconDocument,
7+
Grep: IconSearch,
8+
Bash: IconTerminal,
9+
Edit: IconCode,
10+
PostHog: IconSearch,
11+
}
12+
13+
function UserMessage({ content }: { content: React.ReactNode }) {
14+
return (
15+
<div className="border-l-2 border-red dark:border-yellow bg-accent py-2 pl-3 pr-2">
16+
<div className="font-medium text-sm [&>*:last-child]:mb-0">{content}</div>
17+
</div>
18+
)
19+
}
20+
21+
function ToolCall({
22+
toolName,
23+
toolDetail,
24+
expanded,
25+
}: {
26+
toolName?: string
27+
toolDetail?: string
28+
expanded?: React.ReactNode
29+
}) {
30+
const Icon = toolName ? toolIcons[toolName] || IconTerminal : IconTerminal
31+
32+
return (
33+
<div className="pl-3 py-0.5">
34+
<div className="flex items-center gap-2">
35+
<Icon className="size-3 text-muted shrink-0" />
36+
<span className="text-xs text-muted font-code truncate">
37+
{toolName}
38+
{toolDetail && <span className="ml-1 opacity-75">{toolDetail}</span>}
39+
</span>
40+
</div>
41+
{expanded && (
42+
<div className="mt-2 ml-5 max-w-4xl overflow-hidden rounded-sm border border-input">{expanded}</div>
43+
)}
44+
</div>
45+
)
46+
}
47+
48+
function ThinkBlock({ content }: { content: React.ReactNode }) {
49+
return (
50+
<div className="my-2 max-w-4xl overflow-hidden rounded-sm border border-input bg-primary">
51+
<div className="px-3 py-2 flex items-center gap-2">
52+
<IconBrain className="size-3 text-muted shrink-0" />
53+
<span className="text-xs text-muted font-code">Thinking...</span>
54+
</div>
55+
{content && (
56+
<div className="px-3 py-2 border-t border-input">
57+
<p className="text-xs text-muted font-code m-0 whitespace-pre-wrap">{content}</p>
58+
</div>
59+
)}
60+
</div>
61+
)
62+
}
63+
64+
function AgentMessage({ content }: { content: React.ReactNode }) {
65+
return (
66+
<div className="py-1 pl-3 pr-2">
67+
<div className="text-sm [&>*:last-child]:mb-0 [&_p]:mb-2 [&_ul]:mb-2 [&_ul]:pl-4 [&_ul]:list-disc [&_li]:mb-1 [&_strong]:font-semibold [&_code]:font-code [&_code]:text-xs [&_code]:bg-accent [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded-sm [&_code]:border [&_code]:border-input">
68+
{content}
69+
</div>
70+
</div>
71+
)
72+
}
73+
74+
function LoadingIndicator() {
75+
return (
76+
<div className="pl-3 py-1.5 flex items-center gap-2">
77+
<span className="text-xs text-muted font-code animate-pulse">Thinking...</span>
78+
</div>
79+
)
80+
}
81+
82+
interface CodeConversationProps {
83+
conversation: ConversationItem[]
84+
activeSection: number
85+
}
86+
87+
export default function CodeConversation({ conversation, activeSection }: CodeConversationProps) {
88+
const scrollRef = useRef<HTMLDivElement>(null)
89+
90+
// Auto-scroll to bottom when conversation updates
91+
useEffect(() => {
92+
const el = scrollRef.current
93+
if (el) {
94+
el.scrollTop = el.scrollHeight
95+
}
96+
}, [conversation.length])
97+
98+
return (
99+
<div ref={scrollRef} key={activeSection} className="flex-1 overflow-y-auto bg-primary">
100+
<div className="pb-16">
101+
{conversation.map((item, index) => (
102+
<div key={index} className="mx-auto max-w-[750px] px-2 py-1.5">
103+
{item.type === 'user' && <UserMessage content={item.content} />}
104+
{item.type === 'tool' && (
105+
<ToolCall toolName={item.toolName} toolDetail={item.toolDetail} expanded={item.expanded} />
106+
)}
107+
{item.type === 'think' && <ThinkBlock content={item.content} />}
108+
{item.type === 'agent' && <AgentMessage content={item.content} />}
109+
{item.type === 'loading' && <LoadingIndicator />}
110+
</div>
111+
))}
112+
</div>
113+
</div>
114+
)
115+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useRef, useEffect } from 'react'
2+
3+
interface CodeEditorProps {
4+
value: string
5+
onChange: (value: string) => void
6+
onSubmit: (value: string) => void
7+
disabled?: boolean
8+
}
9+
10+
export default function CodeEditor({ value, onChange, onSubmit, disabled }: CodeEditorProps) {
11+
const textareaRef = useRef<HTMLTextAreaElement>(null)
12+
13+
// Auto-grow textarea height
14+
useEffect(() => {
15+
const el = textareaRef.current
16+
if (!el) return
17+
el.style.height = 'auto'
18+
el.style.height = `${el.scrollHeight}px`
19+
}, [value])
20+
21+
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
22+
if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && (e.metaKey || e.ctrlKey))) {
23+
e.preventDefault()
24+
if (value.trim() && !disabled) {
25+
onSubmit(value)
26+
}
27+
}
28+
}
29+
30+
return (
31+
<div className="border-t border-input shrink-0 bg-primary">
32+
<div className="mx-auto max-w-[750px] p-2">
33+
<textarea
34+
ref={textareaRef}
35+
value={value}
36+
onChange={(e) => onChange(e.target.value)}
37+
onKeyDown={handleKeyDown}
38+
disabled={disabled}
39+
rows={1}
40+
placeholder="Type a message... @ to mention files, / for skills"
41+
className="w-full resize-none overflow-hidden rounded-sm border border-input bg-primary px-3 py-2.5 text-sm text-primary font-code placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-input disabled:opacity-50 disabled:cursor-not-allowed"
42+
/>
43+
<div className="flex items-center gap-2 mt-1 px-1">
44+
<span className="text-[10px] text-muted font-code">plan mode</span>
45+
<span className="text-[10px] text-muted font-code opacity-50">
46+
Enter to send · Shift+Enter for new line
47+
</span>
48+
</div>
49+
</div>
50+
</div>
51+
)
52+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react'
2+
import { IconSidebarOpen, IconSidebarClose } from '@posthog/icons'
3+
4+
interface CodeHeaderProps {
5+
sidebarOpen: boolean
6+
onToggleSidebar: () => void
7+
title?: string
8+
}
9+
10+
export default function CodeHeader({ sidebarOpen, onToggleSidebar, title }: CodeHeaderProps) {
11+
return (
12+
<div className="flex items-center border-b border-input" style={{ height: '36px', minHeight: '36px' }}>
13+
<div
14+
className="flex items-center justify-between px-2 pr-3 h-full border-r border-input shrink-0 bg-accent"
15+
style={{ width: sidebarOpen ? '260px' : '48px' }}
16+
>
17+
{sidebarOpen && (
18+
<span className="text-xs font-code text-muted font-medium select-none">PostHog Code</span>
19+
)}
20+
<button
21+
onClick={onToggleSidebar}
22+
className="text-muted hover:text-primary transition-colors p-0.5"
23+
aria-label={sidebarOpen ? 'Close sidebar' : 'Open sidebar'}
24+
>
25+
{sidebarOpen ? <IconSidebarClose className="size-4" /> : <IconSidebarOpen className="size-4" />}
26+
</button>
27+
</div>
28+
<div className="flex-1 flex items-center px-3 h-full overflow-hidden">
29+
{title && <span className="text-xs font-code text-secondary font-medium truncate">{title}</span>}
30+
</div>
31+
</div>
32+
)
33+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react'
2+
import { IconPlus, IconFolder } from '@posthog/icons'
3+
import { Section } from './types'
4+
5+
interface CodeSidebarProps {
6+
sections: Section[]
7+
activeSection: number
8+
onSectionClick: (index: number) => void
9+
}
10+
11+
export default function CodeSidebar({ sections, activeSection, onSectionClick }: CodeSidebarProps) {
12+
return (
13+
<div
14+
className="flex flex-col h-full border-r border-input shrink-0 overflow-hidden bg-accent"
15+
style={{ width: '260px' }}
16+
>
17+
{/* New task button (decorative) */}
18+
<div className="px-2 py-1.5">
19+
<div className="flex items-center gap-2 px-2 py-1.5 text-secondary font-code text-xs cursor-default select-none">
20+
<IconPlus className="size-4" />
21+
<span>New task</span>
22+
</div>
23+
</div>
24+
25+
{/* Section label */}
26+
<div className="px-2 py-1">
27+
<span className="font-code text-[10px] uppercase tracking-wide text-muted font-medium px-2">Tasks</span>
28+
</div>
29+
30+
{/* Task list */}
31+
<div className="flex-1 overflow-y-auto">
32+
{sections.map((section, index) => {
33+
const isActive = index === activeSection
34+
const Icon = section.icon
35+
return (
36+
<button
37+
key={section.id}
38+
onClick={() => onSectionClick(index)}
39+
className={`flex items-center gap-2 w-full text-left font-code text-xs py-1.5 px-4 cursor-pointer transition-colors ${
40+
isActive ? 'bg-accent text-primary font-medium' : 'text-secondary hover:bg-accent/50'
41+
}`}
42+
>
43+
{Icon && (
44+
<Icon className={`size-3 shrink-0 ${isActive ? 'text-secondary' : 'text-muted'}`} />
45+
)}
46+
<span className="truncate">{section.title}</span>
47+
</button>
48+
)
49+
})}
50+
</div>
51+
52+
{/* Project label (decorative) */}
53+
<div className="border-t border-input px-3 py-2.5 select-none">
54+
<div className="flex items-center gap-2">
55+
<IconFolder className="size-3 text-muted shrink-0" />
56+
<span className="font-code text-xs text-secondary font-medium truncate">posthog/posthog</span>
57+
</div>
58+
</div>
59+
</div>
60+
)
61+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# PostHogCode
2+
3+
Marketing page for PostHog Code (`/code`) styled to look like the PostHog Code desktop app.
4+
5+
## Architecture
6+
7+
This is an "app-less" window (no `<Editor />`, `<Reader />`, or `<Presentation />` wrapper). The component renders its own internal chrome that mimics the real PostHog Code desktop app layout.
8+
9+
### Components
10+
11+
| Component | File | Description |
12+
|-----------|------|-------------|
13+
| `PostHogCode` | `index.tsx` | Main layout with state management (active section, sidebar toggle) |
14+
| `CodeHeader` | `CodeHeader.tsx` | 36px header bar with sidebar toggle and section title |
15+
| `CodeSidebar` | `CodeSidebar.tsx` | Left sidebar (260px) with section navigation styled as a task list |
16+
| `CodeConversation` | `CodeConversation.tsx` | Main chat area rendering static conversation items |
17+
| `CodeEditor` | `CodeEditor.tsx` | Decorative message input at bottom (non-functional) |
18+
19+
### Data
20+
21+
`data.tsx` contains all static content structured as `Section[]`. Each section has:
22+
- `id` - unique identifier
23+
- `title` - shown in sidebar and header
24+
- `icon` - displayed next to the title in the sidebar
25+
- `conversation` - array of `ConversationItem` objects (user messages, tool calls, think blocks, agent messages)
26+
27+
### Styling
28+
29+
Maps the real PostHog Code app's Radix Themes styling to posthog.com's Tailwind tokens:
30+
31+
| Real App | posthog.com | Usage |
32+
|----------|-------------|-------|
33+
| `bg-gray-1` | `bg-accent` | Backgrounds |
34+
| `border-gray-6` | `border-input` | Borders |
35+
| `--accent-9` | `border-red dark:border-yellow` | User message left border |
36+
| Berkeley Mono | `font-code` | All text |
37+
| `text-gray-10/11/12` | `text-muted/secondary/primary` | Text hierarchy |
38+
39+
### Adding content
40+
41+
To add or modify sections, edit `data.tsx`. Each conversation item type renders differently:
42+
43+
- `user` - Gray background with accent left border, medium weight text
44+
- `tool` - Muted text with tool icon, shows tool name and detail string
45+
- `think` - Bordered box with brain icon and "Thinking..." label
46+
- `agent` - Plain prose with JSX content (headings, lists, code, links)
47+
48+
### Related files
49+
50+
- `src/pages/code/index.tsx` - Gatsby page that renders `<PostHogCode />`
51+
- `src/context/App.tsx` - Window settings for `/code` route (900x600 min, 1200x900 max)
52+
- `src/components/Desktop/index.tsx` - Desktop icon entry in `useProductLinks()`
53+
- `src/hooks/useProduct.ts` - Product metadata (handle: `twig`, slug: `code`)

0 commit comments

Comments
 (0)