Skip to content

Commit e2897bc

Browse files
make decision tree a global component (#14166)
* make decision tree a global component * fix callout * update Co-authored-by: Edwin Lim <[email protected]> * feedback * fix rebase Removed redundant sentence about decision trees. --------- Co-authored-by: Edwin Lim <[email protected]>
1 parent 4920d04 commit e2897bc

File tree

5 files changed

+308
-223
lines changed

5 files changed

+308
-223
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ static/scripts/posthog-init.js
2525
*Type.ts
2626
.env.development
2727
.env.*.local
28+
CLAUDE.local.md
2829

2930
# Generated data files
3031
src/data/mcp-tools.json
Lines changed: 62 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,94 @@
1-
import React, { useState, useEffect } from 'react'
2-
import { IconCheck, IconArrowLeft, IconArrowRight, IconRevert } from '@posthog/icons'
3-
import OSButton from 'components/OSButton'
1+
import React from 'react'
2+
import { DecisionTree } from 'components/Docs/DecisionTree'
3+
import type { DecisionTreeQuestion, DecisionTreeRecommendation } from 'components/Docs/DecisionTree'
44

5-
interface Answer {
6-
platform?: string
7-
framework?: string
8-
server?: string
9-
}
10-
11-
interface Recommendation {
12-
method: string
13-
path: string
14-
reason: string
15-
}
16-
17-
interface Option {
18-
value: string
19-
label: string
20-
}
21-
22-
interface Question {
23-
id: keyof Answer
24-
question: string
25-
description: string
26-
condition?: (answers: Answer) => boolean
27-
options: Option[]
28-
}
5+
const questions: DecisionTreeQuestion[] = [
6+
{
7+
id: 'platform',
8+
question: 'Where are you deploying?',
9+
description: 'Select your hosting platform or deployment target.',
10+
options: [
11+
{ value: 'vercel', label: 'Vercel' },
12+
{ value: 'netlify', label: 'Netlify' },
13+
{ value: 'aws', label: 'AWS' },
14+
{ value: 'cloudflare', label: 'Cloudflare' },
15+
{ value: 'railway', label: 'Railway' },
16+
{ value: 'self-hosted', label: 'Self-hosted server' },
17+
{ value: 'other', label: 'Other / Framework-specific' },
18+
],
19+
},
20+
{
21+
id: 'framework',
22+
question: 'What framework are you using?',
23+
description: 'Some frameworks have built-in proxy capabilities.',
24+
condition: (answers) =>
25+
answers.platform === 'vercel' || answers.platform === 'netlify' || answers.platform === 'other',
26+
options: [
27+
{ value: 'nextjs', label: 'Next.js' },
28+
{ value: 'nuxt', label: 'Nuxt' },
29+
{ value: 'sveltekit', label: 'SvelteKit' },
30+
{ value: 'remix', label: 'Remix' },
31+
{ value: 'other', label: 'Other / Plain HTML' },
32+
],
33+
},
34+
{
35+
id: 'server',
36+
question: 'What server are you using?',
37+
description: 'Select your server or container orchestration platform.',
38+
condition: (answers) => answers.platform === 'self-hosted',
39+
options: [
40+
{ value: 'nginx', label: 'nginx' },
41+
{ value: 'caddy', label: 'Caddy' },
42+
{ value: 'kubernetes', label: 'Kubernetes' },
43+
{ value: 'node', label: 'Node.js' },
44+
{ value: 'other', label: 'Other' },
45+
],
46+
},
47+
]
2948

30-
const getRecommendation = (answers: Answer): Recommendation => {
49+
const getRecommendation = (answers: Record<string, string>): DecisionTreeRecommendation => {
3150
if (answers.platform === 'vercel') {
3251
if (answers.framework === 'nextjs') {
3352
return {
34-
method: 'Next.js rewrites',
53+
title: 'We recommend: Next.js rewrites',
3554
path: '/docs/advanced/proxy/nextjs',
3655
reason: 'Next.js rewrites are built into the framework and work seamlessly on Vercel with minimal configuration.',
3756
}
3857
}
3958
return {
40-
method: 'Vercel rewrites',
59+
title: 'We recommend: Vercel rewrites',
4160
path: '/docs/advanced/proxy/vercel',
4261
reason: 'Vercel rewrites work for any framework on their platform with just a simple vercel.json configuration.',
4362
}
4463
}
4564

4665
if (answers.platform === 'netlify') {
4766
return {
48-
method: 'Netlify redirects',
67+
title: 'We recommend: Netlify redirects',
4968
path: '/docs/advanced/proxy/netlify',
5069
reason: "Netlify's redirect rules work with any framework through a simple netlify.toml or _redirects file.",
5170
}
5271
}
5372

5473
if (answers.platform === 'aws') {
5574
return {
56-
method: 'AWS CloudFront',
75+
title: 'We recommend: AWS CloudFront',
5776
path: '/docs/advanced/proxy/cloudfront',
5877
reason: 'CloudFront integrates seamlessly with AWS services and provides excellent performance with global edge locations.',
5978
}
6079
}
6180

6281
if (answers.platform === 'cloudflare') {
6382
return {
64-
method: 'Cloudflare Workers',
83+
title: 'We recommend: Cloudflare Workers',
6584
path: '/docs/advanced/proxy/cloudflare',
6685
reason: "Workers run at the edge across Cloudflare's global network and work on the free tier.",
6786
}
6887
}
6988

7089
if (answers.platform === 'railway') {
7190
return {
72-
method: 'Railway template',
91+
title: 'We recommend: Railway template',
7392
path: '/docs/advanced/proxy/railway',
7493
reason: 'Railway offers a one-click deployment template that sets up a PostHog proxy instantly.',
7594
}
@@ -78,28 +97,28 @@ const getRecommendation = (answers: Answer): Recommendation => {
7897
if (answers.platform === 'self-hosted') {
7998
if (answers.server === 'kubernetes') {
8099
return {
81-
method: 'Kubernetes Ingress',
100+
title: 'We recommend: Kubernetes Ingress',
82101
path: '/docs/advanced/proxy/kubernetes-ingress-controller',
83102
reason: 'Using an Ingress Controller is the Kubernetes-native way to handle routing and proxy requests.',
84103
}
85104
}
86105
if (answers.server === 'nginx') {
87106
return {
88-
method: 'nginx',
107+
title: 'We recommend: nginx',
89108
path: '/docs/advanced/proxy/nginx',
90109
reason: 'nginx is a battle-tested, high-performance reverse proxy perfect for self-hosted setups.',
91110
}
92111
}
93112
if (answers.server === 'caddy') {
94113
return {
95-
method: 'Caddy',
114+
title: 'We recommend: Caddy',
96115
path: '/docs/advanced/proxy/caddy',
97116
reason: 'Caddy is a modern web server with automatic HTTPS that is simpler to configure than traditional servers.',
98117
}
99118
}
100119
if (answers.server === 'node') {
101120
return {
102-
method: 'Node.js HTTP',
121+
title: 'We recommend: Node.js HTTP',
103122
path: '/docs/advanced/proxy/node',
104123
reason: 'Using node:http lets you integrate the proxy directly into your existing Node.js application.',
105124
}
@@ -108,217 +127,37 @@ const getRecommendation = (answers: Answer): Recommendation => {
108127

109128
if (answers.framework === 'nuxt') {
110129
return {
111-
method: 'Nuxt server routes',
130+
title: 'We recommend: Nuxt server routes',
112131
path: '/docs/advanced/proxy/nuxt',
113132
reason: "Server routes are Nuxt's way to intercept and handle requests on the server side.",
114133
}
115134
}
116135

117136
if (answers.framework === 'sveltekit') {
118137
return {
119-
method: 'SvelteKit server hooks',
138+
title: 'We recommend: SvelteKit server hooks',
120139
path: '/docs/advanced/proxy/sveltekit',
121140
reason: "Server hooks are SvelteKit's way to intercept and handle requests on the server side.",
122141
}
123142
}
124143

125144
if (answers.framework === 'remix') {
126145
return {
127-
method: 'Remix resource routes',
146+
title: 'We recommend: Remix resource routes',
128147
path: '/docs/advanced/proxy/remix',
129148
reason: "Remix's resource routes with splat routing provide a clean, server-side way to proxy requests.",
130149
}
131150
}
132151

133152
return {
134-
method: 'Managed reverse proxy',
153+
title: 'We recommend: Managed reverse proxy',
135154
path: '/docs/advanced/proxy/managed-reverse-proxy',
136155
reason: "Based on your setup, we recommend starting with the managed proxy. It's the simplest option that works everywhere.",
137156
}
138157
}
139158

140-
const questions: Question[] = [
141-
{
142-
id: 'platform',
143-
question: 'Where are you deploying?',
144-
description: 'Select your hosting platform or deployment target.',
145-
options: [
146-
{ value: 'vercel', label: 'Vercel' },
147-
{ value: 'netlify', label: 'Netlify' },
148-
{ value: 'aws', label: 'AWS' },
149-
{ value: 'cloudflare', label: 'Cloudflare' },
150-
{ value: 'railway', label: 'Railway' },
151-
{ value: 'self-hosted', label: 'Self-hosted server' },
152-
{ value: 'other', label: 'Other / Framework-specific' },
153-
],
154-
},
155-
{
156-
id: 'framework',
157-
question: 'What framework are you using?',
158-
description: 'Some frameworks have built-in proxy capabilities.',
159-
condition: (answers) =>
160-
answers.platform === 'vercel' || answers.platform === 'netlify' || answers.platform === 'other',
161-
options: [
162-
{ value: 'nextjs', label: 'Next.js' },
163-
{ value: 'nuxt', label: 'Nuxt' },
164-
{ value: 'sveltekit', label: 'SvelteKit' },
165-
{ value: 'remix', label: 'Remix' },
166-
{ value: 'other', label: 'Other / Plain HTML' },
167-
],
168-
},
169-
{
170-
id: 'server',
171-
question: 'What server are you using?',
172-
description: 'Select your server or container orchestration platform.',
173-
condition: (answers) => answers.platform === 'self-hosted',
174-
options: [
175-
{ value: 'nginx', label: 'nginx' },
176-
{ value: 'caddy', label: 'Caddy' },
177-
{ value: 'kubernetes', label: 'Kubernetes' },
178-
{ value: 'node', label: 'Node.js' },
179-
{ value: 'other', label: 'Other' },
180-
],
181-
},
182-
]
183-
184159
const ProxyDecisionTree: React.FC = () => {
185-
const [step, setStep] = useState(0)
186-
const [answers, setAnswers] = useState<Answer>({})
187-
const [recommendation, setRecommendation] = useState<Recommendation | null>(null)
188-
189-
const currentQuestion = questions[step]
190-
191-
const handleAnswer = (questionId: keyof Answer, value: string) => {
192-
const newAnswers = { ...answers, [questionId]: value }
193-
setAnswers(newAnswers)
194-
195-
const nextQuestions = questions.slice(step + 1)
196-
const hasMoreQuestions = nextQuestions.some((q) => !q.condition || q.condition(newAnswers))
197-
198-
if (hasMoreQuestions) {
199-
setStep(step + 1)
200-
} else {
201-
const rec = getRecommendation(newAnswers)
202-
setRecommendation(rec)
203-
}
204-
}
205-
206-
const reset = () => {
207-
setStep(0)
208-
setAnswers({})
209-
setRecommendation(null)
210-
}
211-
212-
const handleBack = () => {
213-
// Find the previous question that was actually answered
214-
let prevStep = step - 1
215-
while (prevStep >= 0) {
216-
const prevQuestion = questions[prevStep]
217-
if (!prevQuestion.condition || prevQuestion.condition(answers)) {
218-
break
219-
}
220-
prevStep--
221-
}
222-
// Clear the answer for the question we're going back to
223-
const prevQuestion = questions[prevStep]
224-
if (prevQuestion) {
225-
const newAnswers = { ...answers }
226-
delete newAnswers[prevQuestion.id]
227-
// Also clear any answers from questions after this one
228-
questions.slice(prevStep + 1).forEach((q) => {
229-
delete newAnswers[q.id]
230-
})
231-
setAnswers(newAnswers)
232-
}
233-
setStep(prevStep)
234-
}
235-
236-
useEffect(() => {
237-
if (currentQuestion && currentQuestion.condition && !currentQuestion.condition(answers)) {
238-
const nextStep = questions.findIndex((q, i) => i > step && (!q.condition || q.condition(answers)))
239-
if (nextStep !== -1) {
240-
setStep(nextStep)
241-
} else {
242-
const rec = getRecommendation(answers)
243-
setRecommendation(rec)
244-
}
245-
}
246-
}, [step, answers])
247-
248-
if (recommendation) {
249-
return (
250-
<div className="border border-primary rounded-md p-5 bg-accent my-4">
251-
<div className="flex items-start gap-4">
252-
<div className="w-10 h-10 rounded-full bg-green flex items-center justify-center flex-shrink-0">
253-
<IconCheck className="w-5 h-5 text-white" />
254-
</div>
255-
<div className="flex-1">
256-
<h4 className="text-lg font-bold m-0 mb-2">We recommend: {recommendation.method}</h4>
257-
<p className="text-[15px] opacity-75 m-0 mb-4">{recommendation.reason}</p>
258-
259-
<div className="flex flex-wrap gap-2">
260-
<OSButton
261-
asLink
262-
to={recommendation.path}
263-
variant="primary"
264-
size="md"
265-
icon={<IconArrowRight className="w-4 h-4" />}
266-
iconPosition="right"
267-
>
268-
Follow the guide
269-
</OSButton>
270-
<OSButton
271-
onClick={reset}
272-
variant="secondary"
273-
size="md"
274-
icon={<IconRevert className="w-4 h-4" />}
275-
>
276-
Start over
277-
</OSButton>
278-
</div>
279-
</div>
280-
</div>
281-
</div>
282-
)
283-
}
284-
285-
if (!currentQuestion) return null
286-
287-
return (
288-
<div className="border border-primary rounded-md p-5 bg-accent my-4">
289-
{step > 0 && (
290-
<OSButton
291-
onClick={handleBack}
292-
variant="default"
293-
size="sm"
294-
icon={<IconArrowLeft className="size-4" />}
295-
className="mb-3 opacity-60 hover:opacity-100"
296-
>
297-
Back
298-
</OSButton>
299-
)}
300-
301-
<div className="mb-4">
302-
<h4 className="text-lg font-bold m-0">{currentQuestion.question}</h4>
303-
<p className="text-[15px] opacity-75 m-0 mt-1">{currentQuestion.description}</p>
304-
</div>
305-
306-
<div className="grid sm:grid-cols-2 gap-2">
307-
{currentQuestion.options.map((option) => (
308-
<OSButton
309-
key={option.value}
310-
onClick={() => handleAnswer(currentQuestion.id, option.value)}
311-
variant="secondary"
312-
size="md"
313-
width="full"
314-
align="left"
315-
>
316-
{option.label}
317-
</OSButton>
318-
))}
319-
</div>
320-
</div>
321-
)
160+
return <DecisionTree questions={questions} getRecommendation={getRecommendation} />
322161
}
323162

324163
export default ProxyDecisionTree

0 commit comments

Comments
 (0)