Skip to content

Commit dd702a7

Browse files
committed
Add guided first-edit activation
1 parent de5c49b commit dd702a7

8 files changed

Lines changed: 517 additions & 5 deletions

File tree

playground/play.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,35 @@
183183
</button>
184184
</nav>
185185

186+
<aside
187+
id="guided-first-edit"
188+
class="guided-first-edit"
189+
aria-labelledby="guided-first-edit-title"
190+
hidden
191+
>
192+
<div class="guided-first-edit-copy">
193+
<span class="guided-first-edit-kicker">Quick edit</span>
194+
<strong id="guided-first-edit-title">Bend the right knee a little less.</strong>
195+
<span id="guided-first-edit-message" aria-live="polite">
196+
Change <code>knee_right: flex 123</code> to <code>115°</code>.
197+
</span>
198+
</div>
199+
<div class="guided-first-edit-actions">
200+
<button id="guided-first-edit-start" class="btn primary sm" type="button">
201+
Adjust knee angle
202+
</button>
203+
<button id="guided-first-edit-share" class="btn primary sm" type="button" hidden>
204+
Share movement
205+
</button>
206+
<button
207+
id="guided-first-edit-dismiss"
208+
class="x"
209+
type="button"
210+
aria-label="Dismiss quick edit"
211+
>×</button>
212+
</div>
213+
</aside>
214+
186215
<main class="layout">
187216
<section class="editor-pane">
188217
<div class="pane-head">

playground/src/analytics.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export const USAGE_EVENT_NAMES = {
1212
promptCopied: "prompt_copied",
1313
movementAttempted: "movement_attempted",
1414
shareCreated: "share_created",
15+
guidedEditShown: "guided_edit_shown",
16+
guidedEditStarted: "guided_edit_started",
17+
guidedEditCompleted: "guided_edit_completed",
18+
guidedEditDismissed: "guided_edit_dismissed",
1519
embedDocsClicked: "embed_docs_clicked",
1620
installCommandCopied: "install_command_copied",
1721
} as const;
@@ -30,6 +34,8 @@ export type RenderTrigger =
3034
export type ShareKind = "preset" | "encoded";
3135
export type PromptLocation = "landing" | "playground";
3236
export type InstallCommand = "embed" | "packages" | "mcp";
37+
export type GuidedEditSurface = "desktop" | "mobile";
38+
export type GuidedEditStage = "edit" | "success";
3339

3440
export interface UsageEventMap {
3541
preset_opened: { source: PresetOpenSource; preset_id: string };
@@ -38,6 +44,16 @@ export interface UsageEventMap {
3844
prompt_copied: { location: PromptLocation };
3945
movement_attempted: Record<string, never>;
4046
share_created: { share_kind: ShareKind };
47+
guided_edit_shown: { experiment: "superhero_first_edit_v1" };
48+
guided_edit_started: {
49+
experiment: "superhero_first_edit_v1";
50+
surface: GuidedEditSurface;
51+
};
52+
guided_edit_completed: { experiment: "superhero_first_edit_v1" };
53+
guided_edit_dismissed: {
54+
experiment: "superhero_first_edit_v1";
55+
stage: GuidedEditStage;
56+
};
4157
embed_docs_clicked: { location: "for_products" };
4258
install_command_copied: {
4359
command: InstallCommand;

playground/src/editor.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,8 @@ export interface PosecodeEditor {
601601
getValue(): string;
602602
setValue(doc: string): void;
603603
focus(): void;
604+
/** Reveal and focus a direct angle spinner for the requested joint action. */
605+
focusAngleControl(joint: string, action: string): boolean;
604606
/** Highlight an inclusive 1-based line range as the active phase; null clears. */
605607
highlightPhase(from: number | null, to?: number): void;
606608
}
@@ -744,6 +746,23 @@ export function createPosecodeEditor(
744746
opts.onJointSelect?.(null, []);
745747
},
746748
focus: () => view.focus(),
749+
focusAngleControl: (joint: string, action: string) => {
750+
const target = findAngleTargets(view.state.doc.toString()).find(
751+
(candidate) =>
752+
candidate.joint === joint && candidate.action === action,
753+
);
754+
if (!target) return false;
755+
view.dispatch({
756+
selection: { anchor: target.angleFrom, head: target.angleTo },
757+
effects: [
758+
setSelectedJoint.of(target.joint),
759+
setActiveAngle.of(target),
760+
EditorView.scrollIntoView(target.angleFrom, { y: "center" }),
761+
],
762+
});
763+
opts.onJointSelect?.(target.joint, expandJoint(target.joint));
764+
return true;
765+
},
747766
highlightPhase: (from: number | null, to?: number) => {
748767
view.dispatch({
749768
effects: setPhaseHighlight.of(
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
trackUsageEvent,
3+
USAGE_EVENT_NAMES,
4+
type GuidedEditSurface,
5+
} from "./analytics.js";
6+
import { findAngleTargets } from "./direct-manipulation.js";
7+
8+
export const GUIDED_FIRST_EDIT_EXPERIMENT = "superhero_first_edit_v1";
9+
10+
export const GUIDED_FIRST_EDIT_TARGET = {
11+
joint: "knee_right",
12+
action: "flex",
13+
initialDegrees: 123,
14+
suggestedDegrees: 115,
15+
} as const;
16+
17+
export type GuidedFirstEditStage =
18+
| "idle"
19+
| "edit"
20+
| "success"
21+
| "dismissed";
22+
23+
export interface GuidedEditFocusRequest {
24+
joint: string;
25+
action: string;
26+
switchToEditor: boolean;
27+
}
28+
29+
function targetDegrees(source: string): number | null {
30+
const target = findAngleTargets(source).find(
31+
({ joint, action }) =>
32+
joint === GUIDED_FIRST_EDIT_TARGET.joint &&
33+
action === GUIDED_FIRST_EDIT_TARGET.action,
34+
);
35+
return target?.degrees ?? null;
36+
}
37+
38+
/** Keep the experiment on the single intended entry point and known source. */
39+
export function shouldOfferGuidedFirstEdit(
40+
pathname: string,
41+
hash: string,
42+
source: string,
43+
): boolean {
44+
return (
45+
pathname === "/play/superhero-landing" &&
46+
hash === "" &&
47+
targetDegrees(source) === GUIDED_FIRST_EDIT_TARGET.initialDegrees
48+
);
49+
}
50+
51+
/**
52+
* Small state machine for the guided activation experiment. Source is inspected
53+
* only in the browser and is never included in analytics properties.
54+
*/
55+
export class GuidedFirstEditSession {
56+
stage: GuidedFirstEditStage = "idle";
57+
private targetWasEdited = false;
58+
private started = false;
59+
60+
offer(): boolean {
61+
if (this.stage !== "idle") return false;
62+
this.stage = "edit";
63+
trackUsageEvent(USAGE_EVENT_NAMES.guidedEditShown, {
64+
experiment: GUIDED_FIRST_EDIT_EXPERIMENT,
65+
});
66+
return true;
67+
}
68+
69+
begin(surface: GuidedEditSurface): GuidedEditFocusRequest | null {
70+
if (this.stage !== "edit") return null;
71+
if (!this.started) {
72+
this.started = true;
73+
trackUsageEvent(USAGE_EVENT_NAMES.guidedEditStarted, {
74+
experiment: GUIDED_FIRST_EDIT_EXPERIMENT,
75+
surface,
76+
});
77+
}
78+
return {
79+
joint: GUIDED_FIRST_EDIT_TARGET.joint,
80+
action: GUIDED_FIRST_EDIT_TARGET.action,
81+
switchToEditor: surface === "mobile",
82+
};
83+
}
84+
85+
noteUserEdit(source: string, userInitiated: boolean): boolean {
86+
if (this.stage !== "edit" || !userInitiated) return false;
87+
const degrees = targetDegrees(source);
88+
this.targetWasEdited =
89+
degrees !== null && degrees !== GUIDED_FIRST_EDIT_TARGET.initialDegrees;
90+
return this.targetWasEdited;
91+
}
92+
93+
/** Call only after the parser has accepted and the viewer has loaded source. */
94+
confirmValidCustomRender(source: string): boolean {
95+
if (this.stage !== "edit" || !this.targetWasEdited) return false;
96+
const degrees = targetDegrees(source);
97+
if (
98+
degrees === null ||
99+
degrees === GUIDED_FIRST_EDIT_TARGET.initialDegrees
100+
) {
101+
return false;
102+
}
103+
this.stage = "success";
104+
trackUsageEvent(USAGE_EVENT_NAMES.guidedEditCompleted, {
105+
experiment: GUIDED_FIRST_EDIT_EXPERIMENT,
106+
});
107+
return true;
108+
}
109+
110+
dismiss(): boolean {
111+
if (this.stage !== "edit" && this.stage !== "success") return false;
112+
const stage = this.stage;
113+
this.stage = "dismissed";
114+
trackUsageEvent(USAGE_EVENT_NAMES.guidedEditDismissed, {
115+
experiment: GUIDED_FIRST_EDIT_EXPERIMENT,
116+
stage,
117+
});
118+
return true;
119+
}
120+
}

0 commit comments

Comments
 (0)