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 modified bun.lockb
Binary file not shown.
468 changes: 460 additions & 8 deletions web/knowledge.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"next-themes": "^0.3.0",
"nextjs-linkedin-insight-tag": "^0.0.6",
"pg": "^8.13.0",
"posthog-js": "^1.205.0",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.53.0",
Expand Down
108 changes: 108 additions & 0 deletions web/src/app/analytics.knowledge.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,113 @@
# Analytics Implementation

## PostHog Integration

Important: When integrating PostHog:
- Initialize after user consent
- Respect Do Not Track browser setting
- Anonymize IP addresses by setting `$ip: null`
- Use React Context to expose reinitialization function instead of reloading page
- Place PostHogProvider above other providers in component tree
- Track events with additional context (theme, referrer, etc.)
- For cookie consent:
- Avoid page reloads which cause UI flicker
- Use context to expose reinitialize function
- Keep consent UI components inside PostHogProvider
- Keep components simple - prefer single component over wrapper when possible
- Place consent UI inside PostHogProvider to access context directly

Example event tracking:
```typescript
posthog.capture('event_name', {
referrer: document.referrer,
theme: theme,
// Add other relevant context
})
```

## Event Tracking Patterns

Important event tracking considerations:
- Include theme context with all events via useTheme hook
- Track location/source of identical actions (e.g., 'copy_action' from different places)
- For terminal interactions, track both the command and its result
- When tracking theme changes, include both old and new theme values
- Avoid theme variable naming conflicts with component state by using aliases (e.g., colorTheme)
- Pass event handlers down as props rather than accessing global posthog in child components

## Event Naming Convention

Event names should be verb-forward, past tense, using spaces. Examples:
- clicked get started
- opened demo video
- viewed terminal help
- changed terminal theme
- executed terminal command

Example event properties:
```typescript
// Click events
{
location: 'hero_section' | 'cta_section' | 'install_dialog',
theme: string,
referrer?: string
}

// Copy events
{
command: string,
location: string,
theme: string
}

// Theme change events
{
from_theme: string,
to_theme: string
}
```

## Component Patterns

When adding analytics to React components:
- Pass event handlers as props (e.g., `onTestimonialClick`) rather than using global PostHog directly
- Avoid naming conflicts with component state by using aliases (e.g., `colorTheme` for theme context)
- Keep all analytics event handlers in the parent component
- Use consistent property names across similar events
- Include component-specific context in event properties (location, action type)

## TypeScript Integration

Important: When integrating PostHog with Next.js:
- Use the official PostHog React provider from 'posthog-js/react'
- Wrap the provider with the PostHog client instance: `<PostHogProvider client={posthog}>`
- Initialize PostHog before using the provider
- Handle cleanup with posthog.shutdown() in useEffect cleanup function
- Respect Do Not Track and user consent before initialization
- Consider disabling automatic pageview tracking and handling it manually for more control

Example setup:
```typescript
'use client'
import posthog from 'posthog-js'
import { PostHogProvider as PHProvider } from 'posthog-js/react'

export function PostHogProvider({ children }) {
useEffect(() => {
if (hasConsent && !doNotTrack) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_API_KEY, {
api_host: 'https://app.posthog.com',
capture_pageview: false,
})
posthog.capture('$pageview')
}
return () => posthog.shutdown()
}, [])

return <PHProvider client={posthog}>{children}</PHProvider>
}
```

## LinkedIn Conversion Tracking

The application implements LinkedIn conversion tracking using a multi-step flow:
Expand Down
249 changes: 249 additions & 0 deletions web/src/app/api/demo/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { env } from '@/env.mjs'
import OpenAI from 'openai'
import { headers } from 'next/headers'
import { z } from 'zod'

// Rate limit: 10 requests per minute per IP
const RATE_LIMIT = 10
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute in milliseconds

// Store IP addresses and their request timestamps
const ipRequests = new Map<string, { count: number; resetTime: number }>()

// Clean up old entries every minute
setInterval(() => {
const now = Date.now()
for (const [ip, data] of ipRequests.entries()) {
if (data.resetTime < now) {
ipRequests.delete(ip)
}
}
}, RATE_LIMIT_WINDOW)

const deepseekClient = new OpenAI({
apiKey: env.DEEPSEEK_API_KEY,
baseURL: 'https://api.deepseek.com',
})

const corsHeaders = {
'Access-Control-Allow-Origin': env.NEXT_PUBLIC_APP_URL,
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Credentials': 'true',
}

// Handle OPTIONS preflight request
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: corsHeaders,
})
}

// Define request schema
const RequestSchema = z.object({
prompt: z.union([
z.string().min(1, { message: 'Prompt is required' }),
z.array(z.string()).min(1, { message: 'At least one prompt is required' }),
]),
})

const cleanCodeBlocks = (
content: string
): { html: string; message: string } => {
// Extract all code blocks
const codeMatches = content.match(/```[\w-]*\n([\s\S]*?)\n```/g) || []

// Get just the code content from each block
const codeContent = codeMatches.reduce((acc, curr) => {
const match = curr.match(/```[\w-]*\n([\s\S]*?)\n```/)
return match ? acc + '\n' + match[1] : acc
}, '')

return {
html: codeContent,
// Keep the original text for the message, replacing code blocks with a placeholder
message: content.replace(
/```[\w-]*\n[\s\S]*?\n```/g,
'- Editing file: web/src/app/page.tsx'
),
}
}

const checkRateLimit = (ip: string): Response | null => {
const now = Date.now()
const requestData = ipRequests.get(ip)

if (requestData) {
// If window has expired, reset count
if (requestData.resetTime < now) {
ipRequests.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW })
} else {
// If we're still within the window, increment count
if (requestData.count >= RATE_LIMIT) {
return new Response(
JSON.stringify({
error: 'Rate limit exceeded. Please try again later.',
}),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
}
)
}
requestData.count++
}
} else {
// First request from this IP
ipRequests.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW })
}

return null
}

const validateRequest = async (
request: Request
): Promise<{
data: z.infer<typeof RequestSchema> | null
error: Response | null
}> => {
try {
const body = await request.json()
const result = RequestSchema.safeParse(body)

if (!result.success) {
return {
data: null,
error: new Response(
JSON.stringify({
error: result.error.issues[0].message,
}),
{
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
}
),
}
}

return { data: result.data, error: null }
} catch (error) {
console.error('Error parsing request:', error)
return {
data: null,
error: new Response(
JSON.stringify({
error: 'Invalid request. Please check your input.',
}),
{
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
}
),
}
}
}

const callDeepseekAPI = async (
prompts: string[]
): Promise<{
html: string
message: string
error: Response | null
}> => {
try {
const response = await deepseekClient.chat.completions.create({
model: 'deepseek-chat',
messages: [
{
role: 'system',
content:
"You are a helpful assistant. Respond with valid HTML body that can be injected into an iframe based on the user's messages below. Don't write comments in the code. Be succinct with your response and focus just on the code, with a brief explanation beforehand. Make sure to wrap the code in backticks (```).",
},
...prompts.map((p) => ({
role: 'user' as const,
content: p,
})),
],
temperature: 0,
})

const { html, message } = cleanCodeBlocks(
response.choices[0]?.message?.content || 'No response generated'
)

return { html, message, error: null }
} catch (error) {
console.error('Error calling Deepseek:', error)
return {
html: '',
message: '',
error: new Response(
JSON.stringify({ error: 'Failed to generate response' }),
{
status: 500,
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
}
),
}
}
}

export async function POST(request: Request) {
// Check origin
const origin = request.headers.get('origin')
if (origin !== env.NEXT_PUBLIC_APP_URL) {
return new Response(JSON.stringify({ error: 'Unauthorized origin' }), {
status: 403,
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
})
}

// Get IP address from headers
const forwardedFor = headers().get('x-forwarded-for')
const ip = forwardedFor?.split(',')[0] || 'unknown'

// Check rate limit
const rateLimitError = checkRateLimit(ip)
if (rateLimitError) return rateLimitError

// Validate request
const { data, error: validationError } = await validateRequest(request)
if (validationError) return validationError
if (!data) {
return new Response(JSON.stringify({ error: 'Invalid request data' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
})
}

// Call Deepseek API
const prompts = Array.isArray(data.prompt) ? data.prompt : [data.prompt]
const { html, message, error: apiError } = await callDeepseekAPI(prompts)
if (apiError) return apiError

return new Response(JSON.stringify({ html, message }), {
headers: {
'Content-Type': 'application/json',
...corsHeaders,
},
})
}
Loading