@posthog/twig-components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/build-assets.mjs +18 -0
- package/dist/AiLab.d.ts +26 -0
- package/dist/AiLab.js +140 -0
- package/dist/BookingLab.d.ts +19 -0
- package/dist/BookingLab.js +100 -0
- package/dist/BrowseStaysPreview.d.ts +7 -0
- package/dist/BrowseStaysPreview.js +6 -0
- package/dist/FilterInspector.d.ts +7 -0
- package/dist/FilterInspector.js +27 -0
- package/dist/FilterLabExercise.d.ts +14 -0
- package/dist/FilterLabExercise.js +78 -0
- package/dist/FinishLabButton.d.ts +3 -0
- package/dist/FinishLabButton.js +5 -0
- package/dist/LabChecklist.d.ts +8 -0
- package/dist/LabChecklist.js +6 -0
- package/dist/LabChoices.d.ts +10 -0
- package/dist/LabChoices.js +7 -0
- package/dist/LabCompletionView.d.ts +8 -0
- package/dist/LabCompletionView.js +43 -0
- package/dist/LabNavigation.d.ts +9 -0
- package/dist/LabNavigation.js +7 -0
- package/dist/PlaygroundController.d.ts +31 -0
- package/dist/PlaygroundController.js +82 -0
- package/dist/PlaygroundPanels.d.ts +32 -0
- package/dist/PlaygroundPanels.js +33 -0
- package/dist/ReplayLab.d.ts +28 -0
- package/dist/ReplayLab.js +70 -0
- package/dist/ReplayRecorder.d.ts +28 -0
- package/dist/ReplayRecorder.js +357 -0
- package/dist/SessionPlayer.d.ts +5 -0
- package/dist/SessionPlayer.js +124 -0
- package/dist/StayCard.d.ts +7 -0
- package/dist/StayCard.js +5 -0
- package/dist/StayFilters.d.ts +14 -0
- package/dist/StayFilters.js +7 -0
- package/dist/StayLab.d.ts +16 -0
- package/dist/StayLab.js +47 -0
- package/dist/ai-lab.d.ts +55 -0
- package/dist/ai-lab.js +130 -0
- package/dist/assets/Halfre.ttf +0 -0
- package/dist/assets/RoundHog-Medium.woff2 +0 -0
- package/dist/assets/RoundHog-SemiBold.woff2 +0 -0
- package/dist/assets/RoundHog.woff2 +0 -0
- package/dist/assets/cabin.jpg +0 -0
- package/dist/assets/cliff.png +0 -0
- package/dist/assets/logo.svg +4 -0
- package/dist/booking-lab.d.ts +53 -0
- package/dist/booking-lab.js +134 -0
- package/dist/catalog.css +259 -0
- package/dist/catalog.d.ts +57 -0
- package/dist/catalog.js +111 -0
- package/dist/filter-lab.d.ts +62 -0
- package/dist/filter-lab.js +133 -0
- package/dist/lab.css +774 -0
- package/dist/playground.d.ts +167 -0
- package/dist/playground.js +91 -0
- package/dist/replay-lab.d.ts +19 -0
- package/dist/replay-lab.js +26 -0
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +1 -0
- package/dist/stay-lab.d.ts +32 -0
- package/dist/stay-lab.js +53 -0
- package/dist/trip-dates.d.ts +11 -0
- package/dist/trip-dates.js +49 -0
- package/docs/components.md +78 -0
- package/docs/integration.md +59 -0
- package/docs/workbench.md +37 -0
- package/package.json +241 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PostHog
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Twig components
|
|
2
|
+
|
|
3
|
+
`@posthog/twig-components` holds reusable pieces of the fictional Twig travel site and its PostHog playground. It is a React package, not a complete website.
|
|
4
|
+
|
|
5
|
+
| Need | Start here |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Find a component, its purpose, props, and current use | [Component reference](docs/components.md) |
|
|
8
|
+
| Contribute with a coding agent | [Agent guidance](AGENTS.md) |
|
|
9
|
+
| Report a security issue | [Private vulnerability reporting](https://github.com/PostHog/twig-components/security/advisories/new) |
|
|
10
|
+
| Decide which repo to change for a new lab or guide | [Integration and authoring scenarios](docs/integration.md#authoring-scenarios) |
|
|
11
|
+
| Understand package setup and updates | [Integration and updates](docs/integration.md) |
|
|
12
|
+
| Develop and click through components | [Workbench guide](docs/workbench.md) |
|
|
13
|
+
| See static release snapshots | `npm run gallery`, then open `gallery/output/index.html` |
|
|
14
|
+
|
|
15
|
+
## How it fits
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart LR
|
|
19
|
+
package["twig-components<br/>UI, data, lab logic, styles"]
|
|
20
|
+
twig["twig.com<br/>pages and routing"]
|
|
21
|
+
guide["posthog.com<br/>PostHog 101 guide"]
|
|
22
|
+
package --> twig
|
|
23
|
+
package -. "planned read-only Twig views" .-> guide
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Current status:** Twig.com imports a local workspace copy of this package. The standalone package is not published to npm. PostHog 101 does not yet import it. See [integration and updates](docs/integration.md) before changing either site.
|
|
27
|
+
|
|
28
|
+
## Quick example
|
|
29
|
+
|
|
30
|
+
`BrowseStaysPreview` is a static Twig excerpt for an article. `selected` controls which filter and stay it shows; it does not handle clicks or record events.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { BrowseStaysPreview } from "@posthog/twig-components/browse-stays-preview";
|
|
34
|
+
import "@posthog/twig-components/catalog.css";
|
|
35
|
+
|
|
36
|
+
<BrowseStaysPreview selected="Coast" />;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For the interactive website filters, use [`StayFilters`](docs/components.md#website-views-and-data) instead. For the five teaching labs and playground shell, see [labs and playground](docs/components.md#labs-and-playground).
|
|
40
|
+
|
|
41
|
+
## Work locally
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm ci
|
|
45
|
+
npm run workbench
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Open the URL printed in the terminal. The workbench shows Twig views and all five labs, with local fixtures you can click through. See the [workbench guide](docs/workbench.md) for adding a component or lab preview. The gallery remains a static snapshot for release review.
|
|
49
|
+
|
|
50
|
+
Before asking for review:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm test
|
|
54
|
+
npm run workbench:build
|
|
55
|
+
npm run gallery
|
|
56
|
+
npm pack --dry-run
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`npm test` builds the package and runs its tests. `workbench:build` checks the live preview. `npm pack --dry-run` lists the files a release would contain. Changes here do not update either website automatically.
|
package/build-assets.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
2
|
+
await mkdir(new URL("./dist/assets/", import.meta.url), { recursive: true });
|
|
3
|
+
for (const [source, target] of [
|
|
4
|
+
["./src/catalog.css", "catalog.css"],
|
|
5
|
+
["./src/lab.css", "lab.css"],
|
|
6
|
+
["./assets/Halfre.ttf", "assets/Halfre.ttf"],
|
|
7
|
+
["./assets/logo.svg", "assets/logo.svg"],
|
|
8
|
+
["./assets/cliff.png", "assets/cliff.png"],
|
|
9
|
+
["./assets/cabin.jpg", "assets/cabin.jpg"],
|
|
10
|
+
["./assets/RoundHog.woff2", "assets/RoundHog.woff2"],
|
|
11
|
+
["./assets/RoundHog-Medium.woff2", "assets/RoundHog-Medium.woff2"],
|
|
12
|
+
["./assets/RoundHog-SemiBold.woff2", "assets/RoundHog-SemiBold.woff2"],
|
|
13
|
+
]) {
|
|
14
|
+
await copyFile(
|
|
15
|
+
new URL(source, import.meta.url),
|
|
16
|
+
new URL(`./dist/${target}`, import.meta.url)
|
|
17
|
+
);
|
|
18
|
+
}
|
package/dist/AiLab.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type AiAction, type AiScenario, type AiState } from "./ai-lab.js";
|
|
3
|
+
type Lesson = "example" | "observe" | "repair" | "timeout" | "explore";
|
|
4
|
+
export declare function AiLabProvider({ children, active, }: {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
active: boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare const useAiLab: () => {
|
|
9
|
+
state: AiState;
|
|
10
|
+
dispatch: (action: AiAction) => void;
|
|
11
|
+
active: boolean;
|
|
12
|
+
scenario: AiScenario;
|
|
13
|
+
setScenario: (scenario: AiScenario) => void;
|
|
14
|
+
lesson: Lesson;
|
|
15
|
+
setLesson: (lesson: Lesson) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function useResetAiLab(): () => void;
|
|
18
|
+
export declare function AiIntroduction({ onContinue }: {
|
|
19
|
+
onContinue: () => void;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export declare function AiLab({ stage, onStage, onFocusPlanner, }: {
|
|
22
|
+
stage: number;
|
|
23
|
+
onStage: (stage: number) => void;
|
|
24
|
+
onFocusPlanner: () => void;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
26
|
+
export {};
|
package/dist/AiLab.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { LabChoices } from "./LabChoices.js";
|
|
4
|
+
import { FinishLabButton } from "./FinishLabButton.js";
|
|
5
|
+
import { LabChecklist } from "./LabChecklist.js";
|
|
6
|
+
import { createContext, useContext, useReducer, useState, } from "react";
|
|
7
|
+
import { aiReducer, recommendationEvidence, aiCode, fixtureRequest, initialAiState, } from "./ai-lab.js";
|
|
8
|
+
const AiContext = createContext({
|
|
9
|
+
state: initialAiState,
|
|
10
|
+
dispatch: () => { },
|
|
11
|
+
active: false,
|
|
12
|
+
scenario: "success",
|
|
13
|
+
setScenario: () => { },
|
|
14
|
+
lesson: "example",
|
|
15
|
+
setLesson: () => { },
|
|
16
|
+
});
|
|
17
|
+
export function AiLabProvider({ children, active, }) {
|
|
18
|
+
const [state, dispatch] = useReducer(aiReducer, initialAiState);
|
|
19
|
+
const [scenario, setScenario] = useState("success");
|
|
20
|
+
const [lesson, setLesson] = useState("example");
|
|
21
|
+
return (_jsx(AiContext.Provider, { value: {
|
|
22
|
+
state,
|
|
23
|
+
dispatch,
|
|
24
|
+
active,
|
|
25
|
+
scenario,
|
|
26
|
+
setScenario,
|
|
27
|
+
lesson,
|
|
28
|
+
setLesson,
|
|
29
|
+
}, children: children }));
|
|
30
|
+
}
|
|
31
|
+
export const useAiLab = () => useContext(AiContext);
|
|
32
|
+
export function useResetAiLab() {
|
|
33
|
+
const { dispatch, setScenario, setLesson } = useContext(AiContext);
|
|
34
|
+
return () => {
|
|
35
|
+
dispatch({ type: "reset" });
|
|
36
|
+
setScenario("success");
|
|
37
|
+
setLesson("example");
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function AiIntroduction({ onContinue }) {
|
|
41
|
+
return (_jsxs("div", { className: "vac-guide-intro", children: [_jsx("h3", { children: "Can you investigate a bad recommendation?" }), _jsx("p", { children: "A visitor says Twig suggested the wrong stay. What would you need to see to investigate?" }), _jsx("div", { className: "vac-guide-example vac-guide-lesson", children: _jsx("p", { children: "Follow a request from Twig into a captured event. Then connect the model\u2019s response and see the difference." }) }), _jsx("p", { children: "The goal: compare what the visitor asked with what Twig recommended." }), _jsx("button", { className: "vac-button", onClick: onContinue, children: _jsx("span", { className: "vac-os-button-face", children: "See the worked example \u2192" }) }), _jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "How this simulation works" }), _jsx("p", { children: "Twig uses a preset request and recommendation. You\u2019re changing what gets recorded, not the recommendation itself. No model runs and nothing is sent to PostHog." })] })] }));
|
|
42
|
+
}
|
|
43
|
+
const fields = [
|
|
44
|
+
{ key: "input", label: "Request", detail: "$ai_input" },
|
|
45
|
+
{ key: "output", label: "Response", detail: "$ai_output_choices" },
|
|
46
|
+
{ key: "linked", label: "Trip trace ID", detail: "$ai_trace_id" },
|
|
47
|
+
{ key: "errors", label: "Error details", detail: "$ai_is_error + $ai_error" },
|
|
48
|
+
];
|
|
49
|
+
function CaptureMapping({ config }) {
|
|
50
|
+
return (_jsxs("div", { className: "vac-ai-mapping", children: [_jsx("span", { className: "vac-muted", children: "Inside the generation event\u2019s properties" }), _jsx("pre", { children: _jsxs("code", { children: ["{\n", config.input && (_jsx("mark", { className: config.output ? "vac-ai-code-context" : "vac-ai-code-highlight", children: ' $ai_input: [\n { role: "user", content: request }\n ],\n' })), config.output && (_jsx("mark", { className: "vac-ai-code-highlight", children: ' $ai_output_choices: [\n { role: "assistant", content: response }\n ],\n' })), " // Other fields omitted\n}"] }) })] }));
|
|
51
|
+
}
|
|
52
|
+
export function AiLab({ stage, onStage, onFocusPlanner, }) {
|
|
53
|
+
const { state, dispatch, scenario, setScenario, lesson, setLesson } = useContext(AiContext);
|
|
54
|
+
const [selected, select] = useState(null);
|
|
55
|
+
const run = selected && state.runs.includes(selected) ? selected : state.runs.at(-1);
|
|
56
|
+
return (_jsxs("section", { className: "vac-lab vac-builder vac-ai-lab", children: [stage === 1 ? (_jsx(_Fragment, { children: lesson === "example" ? (_jsxs(_Fragment, { children: [_jsx("h3", { children: "From Twig to an event" }), _jsxs("div", { className: "vac-ai-compare", children: [_jsx("h4", { children: "Twig\u2019s request" }), _jsx("p", { children: fixtureRequest })] }), _jsxs("p", { children: ["The app holds that text in ", _jsx("code", { children: "request" }), ". This capture code puts it in PostHog\u2019s ", _jsx("code", { children: "$ai_input" }), " field."] }), _jsx(CaptureMapping, { config: initialAiState.config }), _jsx("p", { children: "Run it once to see those values in the recorded event." }), _jsx("button", { className: "vac-button", onClick: () => {
|
|
57
|
+
dispatch({ type: "edit", config: initialAiState.config });
|
|
58
|
+
dispatch({ type: "apply" });
|
|
59
|
+
setScenario("success");
|
|
60
|
+
setLesson("observe");
|
|
61
|
+
onFocusPlanner();
|
|
62
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "Apply the code \u2192" }) })] })) : !state.applied ? (_jsxs(_Fragment, { children: [_jsx("h3", { children: lesson === "repair"
|
|
63
|
+
? "Connect the response"
|
|
64
|
+
: lesson === "timeout"
|
|
65
|
+
? "Record a failed model request"
|
|
66
|
+
: "Explore captured fields" }), _jsx("p", { children: lesson === "repair" ? (_jsxs(_Fragment, { children: ["Twig holds the recommendation in ", _jsx("code", { children: "response" }), ". Connect it to ", _jsx("code", { children: "$ai_output_choices" }), " to record it too."] })) : lesson === "timeout" ? (_jsxs(_Fragment, { children: ["The next request will time out. Enable", " ", _jsx("strong", { children: "Error details" }), " to capture why it failed."] })) : ("Choose the fields that help answer your question. These fields are optional.") }), _jsxs("fieldset", { className: "vac-ai-fields", children: [_jsx("legend", { children: lesson === "repair"
|
|
67
|
+
? "Add to the capture code"
|
|
68
|
+
: "Choose your fields" }), fields
|
|
69
|
+
.filter((field) => lesson === "repair"
|
|
70
|
+
? field.key === "output"
|
|
71
|
+
: lesson === "timeout"
|
|
72
|
+
? field.key === "errors"
|
|
73
|
+
: true)
|
|
74
|
+
.map((field) => (_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: state.config[field.key], onChange: (e) => dispatch({
|
|
75
|
+
type: "edit",
|
|
76
|
+
config: { [field.key]: e.target.checked },
|
|
77
|
+
}) }), _jsxs("span", { children: [lesson === "repair"
|
|
78
|
+
? "Record the model’s response"
|
|
79
|
+
: field.label, _jsx("code", { children: field.detail })] })] }, field.key)))] }), lesson === "explore" && (_jsx(LabChoices, { label: "Response for the next exercise", value: scenario, onChange: setScenario, options: [
|
|
80
|
+
{ value: "success", label: "Recommendation" },
|
|
81
|
+
{ value: "timeout", label: "Provider timeout" },
|
|
82
|
+
] })), lesson === "timeout" ? (_jsx("pre", { children: state.config.errors
|
|
83
|
+
? `$ai_is_error: Boolean(error),\n$ai_error: error?.message`
|
|
84
|
+
: "// Error fields are not captured yet" })) : (_jsx(CaptureMapping, { config: state.config })), lesson !== "timeout" && (_jsx("p", { className: "vac-muted", children: state.config.output
|
|
85
|
+
? "The highlighted response field will be included on the next run."
|
|
86
|
+
: "The response field is absent from this capture code." })), _jsx("button", { className: "vac-button", disabled: (lesson === "repair" && !state.config.output) ||
|
|
87
|
+
(lesson === "timeout" && !state.config.errors), onClick: () => {
|
|
88
|
+
dispatch({ type: "apply" });
|
|
89
|
+
onFocusPlanner();
|
|
90
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "Apply the change \u2192" }) })] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { children: state.awaitingRun
|
|
91
|
+
? scenario === "timeout"
|
|
92
|
+
? "Try the failed request on Twig"
|
|
93
|
+
: "Run the request on Twig"
|
|
94
|
+
: "Your request is ready to review" }), _jsx("p", { children: state.awaitingRun ? (_jsxs(_Fragment, { children: ["Code applied. Click ", _jsx("strong", { children: "Run simulation" }), " in Twig\u2019s trip planner."] })) : ("The request has finished. Review what your capture code recorded.") }), _jsx(LabChecklist, { label: "AI exercise progress", items: [
|
|
95
|
+
{ label: "Apply the capture code", done: true },
|
|
96
|
+
{
|
|
97
|
+
label: (_jsxs(_Fragment, { children: ["Click ", _jsx("strong", { children: "Run simulation" }), " on Twig"] })),
|
|
98
|
+
done: !state.awaitingRun,
|
|
99
|
+
},
|
|
100
|
+
] }), state.awaitingRun ? (_jsx("button", { className: "vac-button", onClick: onFocusPlanner, children: _jsx("span", { className: "vac-os-button-face", children: "Show the trip planner \u2192" }) })) : (_jsx("button", { className: "vac-button", onClick: () => {
|
|
101
|
+
select(null);
|
|
102
|
+
onStage(2);
|
|
103
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "See what was recorded \u2192" }) }))] })) })) : (_jsxs(_Fragment, { children: [_jsx("h3", { children: run?.scenario === "timeout"
|
|
104
|
+
? "Review the failed request"
|
|
105
|
+
: lesson === "observe"
|
|
106
|
+
? "Find the missing response"
|
|
107
|
+
: "Compare the request and response" }), !run ? (_jsxs(_Fragment, { children: [_jsx("p", { children: "Run the worked example in the exercise first." }), _jsx("button", { className: "vac-button", onClick: () => onStage(1), children: _jsx("span", { className: "vac-os-button-face", children: "Continue the exercise \u2192" }) })] })) : (_jsxs(_Fragment, { children: [state.runs.length > 1 && (_jsx(LabChoices, { label: "Compare runs", value: run.id, onChange: (id) => select(state.runs.find((item) => item.id === id) ?? null), options: [...state.runs].reverse().map((item) => ({
|
|
108
|
+
value: item.id,
|
|
109
|
+
label: `Run ${item.id} · ${item.scenario === "success" ? "Recommendation" : "Timeout"}`,
|
|
110
|
+
})) })), _jsxs("div", { className: "vac-ai-compare", children: [_jsx("h4", { children: "Recorded evidence" }), _jsxs("dl", { children: [_jsxs("dt", { children: ["Visitor\u2019s request \u00B7 ", _jsx("code", { children: "$ai_input" })] }), _jsx("dd", { children: run.config.input ? (_jsx("mark", { className: "vac-ai-code-highlight", children: run.prompt })) : ("Not captured") }), _jsx("dt", { children: run.response ? (_jsxs(_Fragment, { children: ["Model\u2019s response \u00B7 ", _jsx("code", { children: "$ai_output_choices" })] })) : ("Model error") }), _jsx("dd", { children: run.response ? (run.config.output ? (_jsx("mark", { className: "vac-ai-code-highlight", children: run.response })) : ("Not captured")) : run.config.errors ? ("Simulated provider timeout") : ("Not captured") })] })] }), _jsxs("div", { className: "vac-lab-output", role: "status", children: [lesson === "observe" && (_jsx("strong", { children: "Twig displayed a recommendation, but this code never recorded it." })), _jsx("p", { children: recommendationEvidence(run).explanation })] }), lesson === "timeout" &&
|
|
111
|
+
run.scenario === "timeout" &&
|
|
112
|
+
recommendationEvidence(run).complete ? (_jsx(FinishLabButton, { onClick: () => onStage(3) })) : (_jsx("button", { className: "vac-button", onClick: () => {
|
|
113
|
+
select(null);
|
|
114
|
+
const next = lesson === "observe"
|
|
115
|
+
? "repair"
|
|
116
|
+
: lesson === "repair" &&
|
|
117
|
+
recommendationEvidence(run).complete
|
|
118
|
+
? "timeout"
|
|
119
|
+
: lesson === "repair"
|
|
120
|
+
? "repair"
|
|
121
|
+
: "explore";
|
|
122
|
+
setLesson(next);
|
|
123
|
+
setScenario(next === "timeout" ? "timeout" : "success");
|
|
124
|
+
dispatch({
|
|
125
|
+
type: "edit",
|
|
126
|
+
config: next === "repair"
|
|
127
|
+
? { input: true, output: false }
|
|
128
|
+
: next === "timeout"
|
|
129
|
+
? { input: true, output: true, errors: false }
|
|
130
|
+
: {},
|
|
131
|
+
});
|
|
132
|
+
onStage(1);
|
|
133
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: lesson === "observe" ||
|
|
134
|
+
(lesson === "repair" &&
|
|
135
|
+
!recommendationEvidence(run).complete)
|
|
136
|
+
? "Connect the missing response →"
|
|
137
|
+
: lesson === "repair"
|
|
138
|
+
? "Record a failed request →"
|
|
139
|
+
: "Explore another configuration →" }) })), recommendationEvidence(run).complete && (_jsx("p", { className: "vac-muted", children: "You can now compare the request and outcome. This helps investigate a problem; it doesn\u2019t fix the model\u2019s answer." }))] }))] })), _jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "Code and session options" }), _jsx("pre", { children: aiCode(stage === 2 && run ? run.config : state.applied ?? state.config) }), stage === 2 && run && (_jsxs(_Fragment, { children: [_jsx("h4", { children: "Captured event JSON" }), _jsx("pre", { children: JSON.stringify(run.event, null, 2) })] })), _jsx("a", { href: "https://posthog.com/docs/ai-observability/installation/manual-capture", target: "_blank", rel: "noreferrer", children: "Manual capture docs \u2197" }), _jsxs("div", { className: "vac-lab-actions", children: [stage === 1 && state.applied && lesson !== "observe" && (_jsx("button", { className: "vac-text-button", onClick: () => dispatch({ type: "edit", config: {} }), children: "Edit captured fields" })), state.runs.length > 0 && (_jsx("button", { className: "vac-text-button", onClick: () => dispatch({ type: "clear" }), children: "Clear runs" }))] })] })] }));
|
|
140
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReactNode, type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
import { type TripSelection } from "./trip-dates.js";
|
|
3
|
+
import { type BookingAction, type BookingState } from "./booking-lab.js";
|
|
4
|
+
export declare function BookingLabProvider({ children, active, }: {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
active: boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare const useBookingLab: () => {
|
|
9
|
+
state: BookingState;
|
|
10
|
+
dispatch: (action: BookingAction) => void;
|
|
11
|
+
active: boolean;
|
|
12
|
+
trip: TripSelection;
|
|
13
|
+
setTrip: Dispatch<SetStateAction<TripSelection>>;
|
|
14
|
+
};
|
|
15
|
+
export declare function BookingLab({ stage, onStage, onFocusBooking, }: {
|
|
16
|
+
stage: number;
|
|
17
|
+
onStage: (stage: number) => void;
|
|
18
|
+
onFocusBooking: () => void;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { LabChoices } from "./LabChoices.js";
|
|
4
|
+
import { FinishLabButton } from "./FinishLabButton.js";
|
|
5
|
+
import { createContext, useContext, useReducer, useState, } from "react";
|
|
6
|
+
import { todayAtStay, validateTrip, } from "./trip-dates.js";
|
|
7
|
+
import { bookingReducer, bookingCode, initialBookingState, } from "./booking-lab.js";
|
|
8
|
+
const emptyTrip = { checkIn: "", checkOut: "", guests: "" };
|
|
9
|
+
const BookingContext = createContext({
|
|
10
|
+
state: initialBookingState,
|
|
11
|
+
dispatch: () => { },
|
|
12
|
+
active: false,
|
|
13
|
+
trip: emptyTrip,
|
|
14
|
+
setTrip: () => { },
|
|
15
|
+
});
|
|
16
|
+
export function BookingLabProvider({ children, active, }) {
|
|
17
|
+
const [trip, setTrip] = useState(emptyTrip);
|
|
18
|
+
const [state, dispatch] = useReducer(bookingReducer, initialBookingState);
|
|
19
|
+
return (_jsx(BookingContext.Provider, { value: { state, dispatch, active, trip, setTrip }, children: children }));
|
|
20
|
+
}
|
|
21
|
+
export const useBookingLab = () => useContext(BookingContext);
|
|
22
|
+
function Action({ children, onClick, }) {
|
|
23
|
+
return (_jsx("button", { type: "button", className: "vac-button", onClick: onClick, children: _jsx("span", { className: "vac-os-button-face", children: children }) }));
|
|
24
|
+
}
|
|
25
|
+
export function BookingLab({ stage, onStage, onFocusBooking, }) {
|
|
26
|
+
const { state, dispatch, trip } = useBookingLab();
|
|
27
|
+
const [selected, select] = useState(null);
|
|
28
|
+
const attempts = state.runs.filter((run) => run.outcome === state.lesson);
|
|
29
|
+
const run = attempts.find((run) => run.id === selected) ?? attempts.at(-1);
|
|
30
|
+
const success = state.lesson === "success";
|
|
31
|
+
function apply() {
|
|
32
|
+
dispatch({ type: "apply", source: "confirmation", outcome: state.lesson });
|
|
33
|
+
select(null);
|
|
34
|
+
onFocusBooking();
|
|
35
|
+
}
|
|
36
|
+
if (stage === 0)
|
|
37
|
+
return (_jsxs("div", { className: "vac-guide-intro", children: [_jsx("h3", { children: "Follow a booking from click to confirmation" }), _jsx("p", { children: "First, complete a successful booking and inspect its events. Then repeat the exercise with a failed request." }), _jsxs("div", { className: "vac-guide-example vac-booking-overview", children: [_jsx("strong", { children: "Book stay \u2192 Validate trip \u2192 Receive result" }), _jsx("p", { children: "Record the attempt on click. Record completion only after confirmation." })] }), _jsx(Action, { onClick: () => onStage(1), children: success
|
|
38
|
+
? "Practice a successful booking →"
|
|
39
|
+
: "Continue the failure exercise →" })] }));
|
|
40
|
+
if (stage === 1 && state.applied) {
|
|
41
|
+
const { errors } = validateTrip(trip, Number.MAX_SAFE_INTEGER, todayAtStay());
|
|
42
|
+
const datesReady = !errors.checkIn && !errors.checkOut;
|
|
43
|
+
const guestsReady = !errors.guests;
|
|
44
|
+
const finished = !!state.latest;
|
|
45
|
+
const tasks = [
|
|
46
|
+
{
|
|
47
|
+
label: "Choose check-in and check-out dates",
|
|
48
|
+
done: datesReady || finished,
|
|
49
|
+
},
|
|
50
|
+
{ label: "Choose your guests", done: guestsReady || finished },
|
|
51
|
+
{
|
|
52
|
+
label: (_jsxs(_Fragment, { children: ["Click ", _jsx("strong", { children: "Book stay" }), " on Twig"] })),
|
|
53
|
+
done: finished,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const current = tasks.findIndex((task) => !task.done);
|
|
57
|
+
return (_jsxs("section", { className: "vac-lab vac-builder", children: [_jsx("h3", { children: finished
|
|
58
|
+
? success
|
|
59
|
+
? "Your booking is confirmed"
|
|
60
|
+
: "Your booking request failed"
|
|
61
|
+
: "Make your booking on Twig" }), _jsx("p", { role: "status", children: finished ? ("Your attempt is recorded. Next, review the events it produced.") : (_jsxs(_Fragment, { children: ["Code applied. Use the ", _jsx("strong", { children: "Your stay" }), " form on Twig to complete these steps."] })) }), _jsx("ol", { className: "vac-booking-checklist", "aria-label": "Booking progress", children: tasks.map((task, index) => (_jsxs("li", { "data-state": task.done ? "done" : index === current ? "current" : "upcoming", "aria-current": index === current ? "step" : undefined, children: [_jsx("span", { className: "vac-check-status", "aria-label": task.done
|
|
62
|
+
? "Complete"
|
|
63
|
+
: index === current
|
|
64
|
+
? "Next"
|
|
65
|
+
: "Upcoming", children: task.done ? "✓" : index + 1 }), _jsx("span", { children: task.label })] }, index))) }), finished ? (_jsx(Action, { onClick: () => onStage(2), children: success ? "Inspect the success →" : "Inspect the failure →" })) : (_jsx(Action, { onClick: onFocusBooking, children: "Show the booking form \u2192" })), _jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "Applied code" }), _jsx("pre", { className: "vac-booking-code", children: bookingCode("confirmation") })] })] }));
|
|
66
|
+
}
|
|
67
|
+
if (stage === 1)
|
|
68
|
+
return (_jsxs("section", { className: "vac-lab vac-builder", children: [_jsx("h3", { children: state.latest
|
|
69
|
+
? success
|
|
70
|
+
? "Your booking is confirmed"
|
|
71
|
+
: "Your booking request failed"
|
|
72
|
+
: success
|
|
73
|
+
? "Make a successful booking"
|
|
74
|
+
: "Try a failed booking" }), _jsx("p", { children: success ? (_jsxs(_Fragment, { children: ["This code records events from Twig\u2019s ", _jsx("strong", { children: "Book stay" }), " ", "button. Apply it, then make a simulated booking to see", " ", _jsx("code", { children: "booking_started" }), " on click and", " ", _jsx("code", { children: "booking_completed" }), " after confirmation."] })) : ("This request will fail. Keep the same code: it should record the attempt without recording a completed booking.") }), _jsx("pre", { className: "vac-booking-code", "aria-label": "Booking capture code", children: bookingCode("confirmation") }), _jsx(Action, { onClick: apply, children: "Apply the code \u2192" })] }));
|
|
75
|
+
if (!run)
|
|
76
|
+
return (_jsxs("div", { className: "vac-lab", children: [_jsx("h3", { children: success
|
|
77
|
+
? "Complete the successful booking first"
|
|
78
|
+
: "Try the failed booking first" }), _jsx("p", { children: "Try this scenario on Twig, then inspect its events here." }), _jsx(Action, { onClick: () => onStage(1), children: "Continue the exercise \u2192" })] }));
|
|
79
|
+
const completed = run.events.some((event) => event.event === "booking_completed");
|
|
80
|
+
const mismatch = completed && run.outcome === "failure";
|
|
81
|
+
const before = state.runs.find((item) => item.source === "click" && item.outcome === "failure");
|
|
82
|
+
return (_jsxs("section", { className: "vac-lab vac-builder", children: [_jsx("h3", { children: success
|
|
83
|
+
? "What the successful booking recorded"
|
|
84
|
+
: "What the failed booking recorded" }), attempts.length > 1 && (_jsx(LabChoices, { label: "Booking attempt", value: run.id, onChange: select, options: [...attempts].reverse().map((item) => ({
|
|
85
|
+
value: item.id,
|
|
86
|
+
label: (_jsxs(_Fragment, { children: [item.id, " \u00B7 ", item.outcome, " \u00B7", " ", item.source === "click" ? "on click" : "on confirmation"] })),
|
|
87
|
+
})) })), _jsxs("dl", { className: "vac-event-comparison", children: [_jsxs("div", { children: [_jsx("dt", { children: "Twig\u2019s result" }), _jsx("dd", { children: run.outcome === "success"
|
|
88
|
+
? "Confirmed (simulation)"
|
|
89
|
+
: "Request failed" })] }), _jsxs("div", { "data-mismatch": mismatch, children: [_jsx("dt", { children: "Completion recorded" }), _jsx("dd", { children: completed ? "Yes" : "No" })] })] }), _jsx("p", { role: "status", children: mismatch
|
|
90
|
+
? "This completion is false. The click recorded it before the failed response arrived."
|
|
91
|
+
: run.outcome === "failure"
|
|
92
|
+
? "Correct. The request failed, so no completion was recorded. The started event still shows the attempt."
|
|
93
|
+
: run.source === "click"
|
|
94
|
+
? "These match this time, but the code recorded completion before the response. A failed request would still count as completed."
|
|
95
|
+
: "Correct. Completion was recorded after the successful response." }), _jsx("h4", { children: "This attempt\u2019s funnel" }), _jsx("p", { className: "vac-muted", children: "A funnel tracks which steps were reached. These counts describe one local simulated attempt, not real visitors." }), _jsx("ol", { className: "vac-booking-funnel", children: ["availability_checked", "booking_started", "booking_completed"].map((name) => (_jsxs("li", { children: [_jsx("code", { children: name }), _jsx("strong", { children: run.events.some((event) => event.event === name) ? "1" : "0" })] }, name))) }), mismatch && (_jsx("p", { children: "The funnel reports a completed booking even though Twig rejected it. A chart can only be as accurate as the events you send." })), before && run.source === "confirmation" && run.outcome === "failure" && (_jsxs("div", { className: "vac-guide-example", children: [_jsx("strong", { children: "Same failure, different evidence" }), _jsx("p", { children: "Before: a completion on click. Now: an attempt, with no false completion." })] })), success ? (_jsxs(_Fragment, { children: [_jsx("p", { children: "The attempt and completion both appear because Twig confirmed the booking. Next, use the same code with a failed request." }), _jsx(Action, { onClick: () => {
|
|
96
|
+
dispatch({ type: "begin-failure" });
|
|
97
|
+
select(null);
|
|
98
|
+
onStage(1);
|
|
99
|
+
}, children: "Now practice a failed booking \u2192" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vac-guide-example vac-booking-overview", children: [_jsx("strong", { children: "Same code, different result" }), _jsx("p", { children: "Success: started and completed. Failure: started only. The confirmation check keeps a failed request out of your completed-booking count." })] }), !mismatch && _jsx(FinishLabButton, { onClick: () => onStage(3) })] })), _jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "Captured events and applied code" }), _jsx("p", { children: "Availability is recorded when the local availability check succeeds. Start records the request. Completion depends on the code you applied." }), _jsx("pre", { className: "vac-booking-code", children: bookingCode(run.source) }), _jsx("pre", { children: JSON.stringify(run.events, null, 2) })] })] }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StaySetting } from "./settings.js";
|
|
2
|
+
export type BrowseStaysPreviewProps = {
|
|
3
|
+
selected: Exclude<StaySetting, "All">;
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
/** A read-only excerpt of Twig's Browse stays view for editorial contexts. */
|
|
7
|
+
export declare function BrowseStaysPreview({ selected, className, }: BrowseStaysPreviewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { staySettings } from "./settings.js";
|
|
3
|
+
/** A read-only excerpt of Twig's Browse stays view for editorial contexts. */
|
|
4
|
+
export function BrowseStaysPreview({ selected, className = "", }) {
|
|
5
|
+
return (_jsx("div", { className: `twig-stays-preview ${className}`.trim(), role: "img", "aria-label": `Twig Browse stays with the ${selected} filter selected and one ${selected} stay`, children: _jsxs("div", { className: "twig-stays-preview-inner", children: [_jsx("span", { className: "twig-stays-preview-kicker", children: "Explore" }), _jsx("h3", { children: "Browse stays" }), _jsx("div", { className: "twig-stays-preview-filters", "aria-hidden": "true", children: staySettings.map((setting) => (_jsx("span", { className: "twig-stays-preview-filter", "data-twig-filter": setting, "data-selected": setting === selected, children: setting === "All" ? "All stays" : setting }, setting))) }), _jsx("p", { className: "twig-stays-preview-count", children: "1 stay" }), _jsxs("div", { className: "twig-stays-preview-stay", children: [_jsx("span", { className: "twig-stays-preview-image", "aria-hidden": "true" }), _jsxs("span", { "data-twig-stay": selected, children: [selected, " stay"] })] })] }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { summarizeFilterEvents, } from "./filter-lab.js";
|
|
5
|
+
export function FilterInspector({ state, onPractice, filterHref, bounded = false, }) {
|
|
6
|
+
const detailId = useId();
|
|
7
|
+
const [selection, setSelection] = useState(null);
|
|
8
|
+
const selected = selection && state.events.includes(selection)
|
|
9
|
+
? selection
|
|
10
|
+
: state.events.at(-1);
|
|
11
|
+
const summary = summarizeFilterEvents(state.events);
|
|
12
|
+
const applied = state.applied;
|
|
13
|
+
if (!applied)
|
|
14
|
+
return (_jsxs("div", { className: "vac-lab-output", children: [_jsx("p", { children: "Apply your event in the exercise first. Then click Forest and Coast to compare their events here." }), _jsx("button", { className: "vac-button", onClick: onPractice, children: _jsx("span", { className: "vac-os-button-face", children: "Continue the exercise \u2192" }) })] }));
|
|
15
|
+
if (!selected)
|
|
16
|
+
return (_jsxs("div", { className: "vac-lab-output", children: [_jsx("h4", { children: "No events yet" }), _jsx("p", { children: "Click Forest, then Coast to compare their events." }), _jsx("a", { className: "vac-browse-link", href: filterHref, children: "Go to the filters \u2197" })] }));
|
|
17
|
+
const matches = selected.clicked === selected.properties.destination_type;
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsxs("p", { className: "vac-muted", role: "status", children: [state.events.length, " local events \u00B7 ", summary.mismatches, " ", summary.mismatches === 1 ? "mismatch" : "mismatches"] }), _jsxs("div", { className: "vac-filter-comparisons", role: "group", "aria-label": "Recorded clicks", children: [_jsxs("div", { className: "vac-filter-comparison-head", "aria-hidden": "true", children: [_jsx("span", { children: "Clicked" }), _jsx("span", { children: "Recorded" }), _jsx("span", { children: "Result" })] }), _jsx("div", { className: "vac-filter-comparison-list", children: state.events.map((event) => {
|
|
19
|
+
const match = event.clicked === event.properties.destination_type;
|
|
20
|
+
return (_jsxs("button", { type: "button", className: "vac-filter-comparison-row", "aria-pressed": selected.id === event.id, "aria-controls": detailId, "aria-label": `Event ${event.id}: clicked ${event.clicked}, recorded ${event.properties.destination_type}, ${match ? "match" : "mismatch"}`, onClick: () => setSelection(event), children: [_jsxs("span", { children: [_jsxs("small", { children: ["#", event.id] }), " ", event.clicked] }), _jsx("code", { children: event.properties.destination_type }), _jsx("span", { className: "vac-filter-match", "data-mismatch": !match, children: match ? "Match" : "Mismatch" })] }, event.id));
|
|
21
|
+
}) })] }), _jsxs("section", { id: detailId, className: "vac-lab-output", "aria-label": `Event ${selected.id} comparison`, children: [_jsxs("h4", { children: ["Event #", selected.id] }), _jsx("code", { className: "vac-inspect-name", children: selected.event }), _jsxs("dl", { className: "vac-event-comparison", children: [_jsxs("div", { children: [_jsx("dt", { children: "Clicked on Twig" }), _jsx("dd", { children: selected.clicked })] }), _jsxs("div", { "data-mismatch": !matches, children: [_jsx("dt", { children: "Recorded destination type" }), _jsx("dd", { children: selected.properties.destination_type })] })] }), _jsx("p", { children: matches
|
|
22
|
+
? "These values match."
|
|
23
|
+
: "Mismatch: the event describes a different filter from the one clicked." }), _jsx("p", { className: "vac-muted", children: applied.source === "fixed"
|
|
24
|
+
? `The source is a fixed value: ${applied.fixedValue}. Every click records that value, even when a different filter is selected.`
|
|
25
|
+
: "The value comes from the clicked filter, so it changes with the interaction." })] }), state.before.some((event) => event.clicked === selected.clicked) && (_jsxs("div", { className: "vac-ai-compare", children: [_jsx("h4", { children: "Same click, different instrumentation" }), _jsxs("dl", { children: [_jsx("dt", { children: "Before \u00B7 fixed value" }), _jsxs("dd", { children: [selected.clicked, " \u2192", " ", state.before.find((event) => event.clicked === selected.clicked)
|
|
26
|
+
?.properties.destination_type] }), _jsxs("dt", { children: ["Now \u00B7", " ", applied.source === "clicked" ? "clicked value" : "fixed value"] }), _jsxs("dd", { children: [selected.clicked, " \u2192 ", selected.properties.destination_type] })] })] })), !bounded && applied.source === "fixed" && (_jsx("button", { className: "vac-button", onClick: onPractice, children: _jsx("span", { className: "vac-os-button-face", children: "Connect the clicked value \u2192" }) }))] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type LabAction, type LabState } from "./filter-lab.js";
|
|
2
|
+
export declare function FilterLabIntroduction({ onContinue }: {
|
|
3
|
+
onContinue: () => void;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
export declare function FilterLabExercise({ state, dispatch, stage, setStage, embedSource, onFocusFilters, onContinue, filterHref, }: {
|
|
6
|
+
state: LabState;
|
|
7
|
+
dispatch: (action: LabAction) => void;
|
|
8
|
+
stage: number;
|
|
9
|
+
setStage: (stage: number) => void;
|
|
10
|
+
embedSource?: "fixed" | "clicked";
|
|
11
|
+
onFocusFilters: () => void;
|
|
12
|
+
onContinue: (source: "fixed" | "clicked") => void;
|
|
13
|
+
filterHref: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { LabChoices } from "./LabChoices.js";
|
|
4
|
+
import { LabChecklist } from "./LabChecklist.js";
|
|
5
|
+
import { FilterInspector } from "./FilterInspector.js";
|
|
6
|
+
import { filterLabCode, labPassed } from "./filter-lab.js";
|
|
7
|
+
import { useId } from "react";
|
|
8
|
+
export function FilterLabIntroduction({ onContinue }) {
|
|
9
|
+
return (_jsxs("div", { className: "vac-guide-intro", children: [_jsx("h3", { children: "Why does every click look like Forest?" }), _jsx("p", { children: "Visitors choose different filters, but your events keep recording Forest." }), _jsx("div", { className: "vac-guide-example vac-guide-lesson", children: _jsx("p", { children: "Follow a click into its event, find the fixed value, then connect the filter\u2019s actual value." }) }), _jsx("p", { children: "The goal: record which destination type each visitor chose." }), _jsx("button", { className: "vac-button", onClick: onContinue, children: _jsx("span", { className: "vac-os-button-face", children: "See the worked example \u2192" }) }), _jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "How this simulation works" }), _jsx("p", { children: "After you apply your configuration, this lab records filter clicks locally while the exercise is open. Nothing is sent to PostHog." }), _jsx("p", { children: "Returning to the list pauses recording and keeps your work. Leaving the page resets it." })] })] }));
|
|
10
|
+
}
|
|
11
|
+
function FilterCode({ config }) {
|
|
12
|
+
return (_jsxs("pre", { children: [`// Inside the filter's click handler\nposthog.capture(${JSON.stringify(config.eventName)}, {\n`, _jsx("mark", { className: "vac-ai-code-highlight", children: ` destination_type: ${config.source === "clicked"
|
|
13
|
+
? "selectedDestinationType"
|
|
14
|
+
: JSON.stringify(config.fixedValue)},` }), "\n});"] }));
|
|
15
|
+
}
|
|
16
|
+
export function FilterLabExercise({ state, dispatch, stage, setStage, embedSource, onFocusFilters, onContinue, filterHref, }) {
|
|
17
|
+
const titleId = useId();
|
|
18
|
+
const latest = state.events.at(-1);
|
|
19
|
+
const passed = labPassed(state);
|
|
20
|
+
const example = {
|
|
21
|
+
eventName: "stay_filter_selected",
|
|
22
|
+
source: "fixed",
|
|
23
|
+
fixedValue: "Forest",
|
|
24
|
+
};
|
|
25
|
+
const triedBoth = ["Forest", "Coast"].every((setting) => state.events.some((event) => event.clicked === setting));
|
|
26
|
+
const nextFilter = state.events.some((event) => event.clicked === "Forest")
|
|
27
|
+
? "Coast"
|
|
28
|
+
: "Forest";
|
|
29
|
+
return (_jsxs("section", { className: "vac-lab vac-builder", "aria-labelledby": titleId, children: [_jsx("h3", { id: titleId, children: stage === 2
|
|
30
|
+
? "Compare the click with the event"
|
|
31
|
+
: state.step === 0
|
|
32
|
+
? "From a filter click to an event"
|
|
33
|
+
: state.step === 2
|
|
34
|
+
? "Connect the clicked value"
|
|
35
|
+
: triedBoth
|
|
36
|
+
? "Now compare the evidence"
|
|
37
|
+
: `Try ${nextFilter} on Twig` }), stage === 1 && (_jsxs(_Fragment, { children: [state.step === 0 && (_jsxs(_Fragment, { children: [_jsxs("p", { children: ["When someone clicks a filter, its click handler runs this code. The event name describes the action \u2013", " ", _jsx("code", { children: "destination_type" }), " records which filter it was."] }), _jsx(FilterCode, { config: { ...example, source: embedSource ?? example.source } }), embedSource === "clicked" ? (_jsxs("p", { children: ["The property now reads ", _jsx("code", { children: "selectedDestinationType" }), " ", "from each click. Apply the code, then test", " ", _jsx("strong", { children: "Forest" }), " and ", _jsx("strong", { children: "Coast" }), "."] })) : (_jsxs("p", { children: [_jsx("code", { children: '"Forest"' }), " is a fixed value. First, click", " ", _jsx("strong", { children: "Forest" }), " to see how it becomes an event. Then try ", _jsx("strong", { children: "Coast" }), "."] })), _jsx("button", { className: "vac-button", onClick: () => {
|
|
38
|
+
dispatch({ type: "example" });
|
|
39
|
+
if (embedSource === "clicked") {
|
|
40
|
+
dispatch({ type: "edit", config: { source: "clicked" } });
|
|
41
|
+
dispatch({ type: "apply" });
|
|
42
|
+
}
|
|
43
|
+
onFocusFilters();
|
|
44
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "Apply the code \u2192" }) })] })), state.step === 2 && (_jsxs(_Fragment, { children: [_jsxs("p", { children: ["Twig stores the clicked filter in", " ", _jsx("code", { children: "selectedDestinationType" }), ". Use that value instead of the text ", _jsx("code", { children: '"Forest"' }), "."] }), _jsx(LabChoices, { label: _jsxs(_Fragment, { children: ["Where should ", _jsx("code", { children: "destination_type" }), " come from?"] }), value: state.config.source, onChange: (source) => dispatch({ type: "edit", config: { source } }), options: [
|
|
45
|
+
{
|
|
46
|
+
value: "fixed",
|
|
47
|
+
label: (_jsxs(_Fragment, { children: ["Fixed text: ", _jsx("code", { children: '"Forest"' })] })),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "clicked",
|
|
51
|
+
label: (_jsxs(_Fragment, { children: ["The clicked filter: ", _jsx("code", { children: "selectedDestinationType" })] })),
|
|
52
|
+
},
|
|
53
|
+
] }), _jsx(FilterCode, { config: state.config }), _jsx("p", { className: "vac-muted", children: state.config.source === "clicked"
|
|
54
|
+
? "The highlighted line now reads the value from each click."
|
|
55
|
+
: "This still records Forest for every click." }), _jsx("button", { className: "vac-button", onClick: () => {
|
|
56
|
+
dispatch({ type: "apply" });
|
|
57
|
+
onFocusFilters();
|
|
58
|
+
}, children: _jsx("span", { className: "vac-os-button-face", children: "Apply the change \u2192" }) })] })), state.step === 3 && (_jsxs(_Fragment, { children: [_jsx("p", { children: triedBoth ? ("You’ve clicked both filters. Inspect how those clicks were recorded.") : (_jsxs(_Fragment, { children: ["Click ", _jsx("strong", { children: nextFilter }), " in Twig\u2019s stay filters. Watch the event below update."] })) }), _jsx(LabChecklist, { label: "Filter exercise progress", items: [
|
|
59
|
+
{
|
|
60
|
+
label: (_jsxs(_Fragment, { children: ["Click ", _jsx("strong", { children: "Forest" }), " on Twig"] })),
|
|
61
|
+
done: state.events.some((event) => event.clicked === "Forest"),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: (_jsxs(_Fragment, { children: ["Click ", _jsx("strong", { children: "Coast" }), " on Twig"] })),
|
|
65
|
+
done: state.events.some((event) => event.clicked === "Coast"),
|
|
66
|
+
},
|
|
67
|
+
] }), latest && (_jsxs("p", { role: "status", children: ["Last click: ", _jsx("strong", { children: latest.clicked }), ". Recorded", " ", _jsx("code", { children: "destination_type" }), ":", " ", _jsx("code", { children: latest.properties.destination_type }), "."] })), triedBoth ? (_jsx("button", { className: "vac-button", onClick: () => setStage(2), children: _jsx("span", { className: "vac-os-button-face", children: "Inspect the difference \u2192" }) })) : (_jsx("button", { className: "vac-button", onClick: onFocusFilters, children: _jsx("span", { className: "vac-os-button-face", children: "Go to the filters \u2192" }) }))] }))] })), stage === 2 && (_jsx(FilterInspector, { state: state, filterHref: filterHref, bounded: Boolean(embedSource), onPractice: () => {
|
|
68
|
+
if (state.selected)
|
|
69
|
+
dispatch({ type: "repair" });
|
|
70
|
+
setStage(1);
|
|
71
|
+
} })), embedSource && stage === 2 && triedBoth && (_jsxs("div", { className: "vac-lab-output", children: [_jsx("h4", { children: embedSource === "fixed"
|
|
72
|
+
? "The mismatch is visible"
|
|
73
|
+
: "The values match" }), _jsx("p", { children: embedSource === "fixed"
|
|
74
|
+
? "Continue reading to see how the team fixes this."
|
|
75
|
+
: "Continue reading to see what the team can now measure." }), _jsx("button", { className: "vac-button", onClick: () => onContinue(embedSource), children: _jsx("span", { className: "vac-os-button-face", children: "Continue reading \u2192" }) })] })), !embedSource && passed && stage === 2 && (_jsx("button", { type: "button", className: "vac-button", onClick: () => setStage(3), children: _jsx("span", { className: "vac-os-button-face", children: "Finish lab" }) })), stage === 1 && state.events.length >= 20 && (_jsx("p", { children: "20-event limit reached. Clear the run to continue." })), stage === 1 && (_jsxs("details", { className: "vac-lab-step", children: [_jsx("summary", { children: "Code and session options" }), _jsx("pre", { children: filterLabCode(embedSource && state.step === 0
|
|
76
|
+
? { ...example, source: embedSource }
|
|
77
|
+
: state.applied ?? state.config) }), _jsx("div", { className: "vac-lab-actions", children: state.events.length > 0 && (_jsx("button", { className: "vac-text-button", onClick: () => dispatch({ type: "clear" }), children: "Clear events" })) })] }))] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
export function FinishLabButton({ onClick }) {
|
|
4
|
+
return (_jsx("button", { type: "button", className: "vac-button", onClick: onClick, children: _jsx("span", { className: "vac-os-button-face", children: "Finish lab" }) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function LabChecklist({ label, items, }) {
|
|
4
|
+
const current = items.findIndex((item) => !item.done);
|
|
5
|
+
return (_jsx("ol", { className: "vac-booking-checklist", "aria-label": label, children: items.map((item, index) => (_jsxs("li", { "data-state": item.done ? "done" : index === current ? "current" : "upcoming", "aria-current": index === current ? "step" : undefined, children: [_jsx("span", { className: "vac-check-status", "aria-label": item.done ? "Complete" : index === current ? "Next" : "Upcoming", children: item.done ? "✓" : index + 1 }), _jsx("span", { children: item.label })] }, index))) }));
|
|
6
|
+
}
|