File size: 7,159 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
 * InteractiveWizard — orchestrates the 5-step new-project flow.
 *
 * Owns the shared form state, fetches /health once for branch-cap
 * limits, and chains the create + seed-graph calls on submit.
 * Each step body lives in its own file under ./wizardSteps so
 * individual steps can be tested / iterated in isolation.
 *
 * Submit semantics:
 *   1. POST /experiences            → new experience id
 *   2. POST /experiences/{id}/seed-graph (best-effort)
 *   3. onCreated(id)                 — parent swaps wizard → editor
 *
 * If seed-graph fails the project is still created — we surface a
 * warning toast and let the user open the editor anyway.
 */

import React, { useCallback, useMemo, useState } from "react";
import { Wand2 } from "lucide-react";
import type { InteractiveApi } from "./api";
import { createInteractiveApi } from "./api";
import type { HealthInfo } from "./types";
import { InteractiveApiError } from "./types";
import {
  ErrorBanner, useAsyncResource, useToast,
} from "./ui";
import { WizardShell, type StepDef } from "./WizardShell";
import {
  DEFAULT_WIZARD_FORM,
  toCreatePayload,
  toPlanPayload,
  type WizardForm,
} from "./wizardState";
import { Step0Prompt, step0Valid } from "./wizardSteps/Step0Prompt";
import { Step1Audience, step1Valid } from "./wizardSteps/Step1Audience";
import { Step2Branches, step2Valid } from "./wizardSteps/Step2Branches";
import { Step3Policy, step3Valid } from "./wizardSteps/Step3Policy";
import { Step4Review } from "./wizardSteps/Step4Review";

export interface InteractiveWizardProps {
  backendUrl: string;
  apiKey?: string;
  onCreated: (experienceId: string) => void;
  onCancel: () => void;
}

const STEPS: StepDef[] = [
  { key: "prompt",   label: "Prompt" },
  { key: "audience", label: "Audience" },
  { key: "branches", label: "Choices" },
  { key: "policy",   label: "Policy" },
  { key: "review",   label: "Review" },
];

export function InteractiveWizard({
  backendUrl, apiKey, onCreated, onCancel,
}: InteractiveWizardProps) {
  const api = useMemo<InteractiveApi>(
    () => createInteractiveApi(backendUrl, apiKey),
    [backendUrl, apiKey],
  );
  const toast = useToast();

  const [form, setFormState] = useState<WizardForm>(DEFAULT_WIZARD_FORM);
  const [step, setStep] = useState(0);
  const [submitting, setSubmitting] = useState(false);

  const setForm = useCallback(
    (patch: Partial<WizardForm>) => setFormState((prev) => ({ ...prev, ...patch })),
    [],
  );

  // /health gives us the branch-cap limits for step 2. If it
  // fails (service disabled / network), we fall back to
  // permissive defaults so the wizard stays usable.
  const health = useAsyncResource<HealthInfo>(
    (signal) => api.health(signal),
    [api],
  );

  const limits = useMemo(
    () => health.data?.limits || {
      max_branches: 12,
      max_depth: 6,
      max_nodes_per_experience: 200,
    },
    [health.data],
  );

  const stepValidators = [
    step0Valid, step1Valid, step2Valid, step3Valid, () => true,
  ];
  const canGoNext = stepValidators[step](form);
  const isLast = step === STEPS.length - 1;

  const goNext = useCallback(async () => {
    if (!isLast) {
      setStep((s) => Math.min(s + 1, STEPS.length - 1));
      return;
    }
    setSubmitting(true);
    try {
      const created = await api.createExperience(toCreatePayload(form));
      if (form.interaction_type !== "persona_live_play") {
        try {
          await api.seedGraph(created.id, toPlanPayload(form));
        } catch (seedErr) {
          const e = seedErr as InteractiveApiError;
          toast.toast({
            variant: "warning",
            title: "Project created, but seeding the graph failed",
            message: e.message || "You can re-run seeding from the editor.",
          });
        }
      }
      toast.toast({
        variant: "success",
        title: "Project created",
        message: form.interaction_type === "persona_live_play"
          ? "Opening persona live setup…"
          : "Opening the editor…",
      });
      onCreated(created.id);
    } catch (err) {
      const e = err as InteractiveApiError;
      toast.toast({
        variant: "error",
        title: "Couldn't create the project",
        message: e.message || "Try again or check the backend.",
      });
      setSubmitting(false);
    }
  }, [api, form, isLast, onCreated, toast]);

  const goBack = useCallback(() => {
    if (step === 0) {
      onCancel();
      return;
    }
    setStep((s) => Math.max(0, s - 1));
  }, [step, onCancel]);

  const personaLive = form.interaction_type === "persona_live_play";
  const titles = personaLive ? [
    "Describe the vibe of this live play session",
    "Who's this session for?",
    "Set progression depth",
    "Pick the policy guardrails",
    "Review and launch",
  ] : [
    "Describe your interactive experience",
    "Who's going to watch?",
    "Shape the branching graph",
    "Pick the policy guardrails",
    "Review and create",
  ];
  const subtitles = personaLive ? [
    "Define the session vibe, persona, and mode.",
    "Optional refinements that bias persona replies and unlock pacing.",
    "Numbers are capped to backend limits, but persona mode stays action-driven.",
    "Decides which guardrails the runtime enforces for every turn.",
    "Verify persona session settings, then we'll create and prepare the live session.",
  ] : [
    "A short brief and a target mode is enough to get started.",
    "Optional refinements that bias the planner toward your viewers.",
    "Numbers are capped to whatever your backend allows.",
    "Decides which guardrails the runtime enforces for every viewer turn.",
    "Verify the planner's interpretation, then we'll create + seed the graph.",
  ];

  return (
    <WizardShell
      steps={STEPS}
      activeIndex={step}
      title={titles[step]}
      subtitle={subtitles[step]}
      canGoBack
      canGoNext={canGoNext}
      submitting={submitting}
      nextLabel={isLast ? "Create project" : "Next"}
      onBack={goBack}
      onNext={goNext}
    >
      {health.error && step === 2 && (
        <div className="mb-4">
          <ErrorBanner
            title="Couldn't read backend caps"
            message={`${health.error} — using permissive defaults.`}
            onRetry={health.reload}
          />
        </div>
      )}

      {step === 0 && <Step0Prompt form={form} setForm={setForm} />}
      {step === 1 && <Step1Audience form={form} setForm={setForm} />}
      {step === 2 && <Step2Branches form={form} setForm={setForm} limits={limits} />}
      {step === 3 && <Step3Policy form={form} setForm={setForm} />}
      {step === 4 && <Step4Review form={form} api={api} />}

      {step === 0 && (
        <p className="mt-6 text-xs text-[#777] flex items-center gap-2">
          <Wand2 className="w-3.5 h-3.5 text-[#3ea6ff]" aria-hidden />
          {personaLive
            ? "Persona live mode uses deterministic action recipes and progression unlocks."
            : "The planner will turn this prompt into a branching scene graph you can then edit scene-by-scene."}
        </p>
      )}
    </WizardShell>
  );
}