KinesteX

Trainer Chat Lifecycle (REST)

The full Trainer Chat API lifecycle in TypeScript: create a plan, refine it, confirm it. Key things this example demonstrates:


  • The profile is sent once — it persists server-side.
  • Preferences (duration, equipment, body parts) live in the message — no separate object needed.
  • Confirmation is natural language — workout_plan.action === "CONFIRMED" signals the final plan.
  • The top-level session_id is the persistent chat session — store it and send it on subsequent requests.
trainer-chat.ts
React TS (Web) example:
1const BASE_URL = "https://data.kinestex.com/api";
2
3interface SequenceItem {
4  order: number;
5  type: "warmup" | "exercise" | "cooldown" | "rest";
6  exercise_id: number | null;
7  repeats: number | null;
8  rest_countdown: number | null;
9  countdown: number;
10}
11
12interface TrainerChatResponse {
13  message: string;
14  intent: string;
15  action: "WORKOUT_UPDATED" | "NO_CHANGE" | "QUESTION" | "PROFILE_UPDATED" | "INFO";
16  session_id: string;
17  workout_plan?: {
18    step: "EXERCISE_RECOMMENDATION" | "PLAN_REFINEMENT";
19    action: "CONFIRMED" | null;
20    data: {
21      sequences: SequenceItem[];
22      exercises: Array<{
23        id: number;
24        thumbnail_url: string;
25        video_url: string;
26        translations: Array<{ language: string; title: string; description: string }>;
27      }>;
28      can_undo?: boolean;
29      can_redo?: boolean;
30      estimated_duration_seconds?: number;
31    };
32  };
33}
34
35async function chat(
36  body: Record<string, unknown>,
37  jwtToken: string
38): Promise<TrainerChatResponse> {
39  const response = await fetch(`${BASE_URL}/trainer/chat`, {
40    method: "POST",
41    headers: {
42      Authorization: `Bearer ${jwtToken}`,
43      "Content-Type": "application/json",
44    },
45    body: JSON.stringify(body),
46  });
47
48  if (!response.ok) {
49    const error = await response.json();
50    if (response.status === 403 && error.code === "not_subscribed") {
51      throw new Error("SUBSCRIPTION_REQUIRED"); // open your subscription flow
52    }
53    throw new Error(`Trainer API error: ${JSON.stringify(error)}`);
54  }
55  return response.json();
56}
57
58// Render a plan as text
59function formatPlan(resp: TrainerChatResponse, lang = "en"): string {
60  const plan = resp.workout_plan;
61  if (!plan) return "(no plan this turn)";
62
63  const titles = new Map(
64    plan.data.exercises.map((ex) => [
65      ex.id,
66      ex.translations.find((t) => t.language === lang)?.title ??
67        ex.translations.find((t) => t.language === "en")?.title ??
68        `Exercise #${ex.id}`,
69    ])
70  );
71
72  return plan.data.sequences
73    .map((seq) =>
74      seq.type === "rest"
75        ? `  Rest — ${seq.rest_countdown}s`
76        : `  [${seq.type}] ${titles.get(seq.exercise_id!)} × ${seq.repeats}`
77    )
78    .join("\n");
79}
80
81// Full conversation flow
82async function trainerDemo(jwtToken: string) {
83  // 1. Create: profile sent once; preferences live in the message
84  const created = await chat(
85    {
86      stage: "CREATE_WORKOUT",
87      message: "Create a 30-minute dumbbell workout for chest and triceps",
88      profile_data: {
89        age: 28,
90        weight: 75.0,
91        height: 180.0,
92        gender: "MALE",
93        fitness_goals: ["strength"],
94      },
95    },
96    jwtToken
97  );
98  const sessionId = created.session_id; // persist this
99  console.log(`Trainer: ${created.message}\n${formatPlan(created)}`);
100
101  // 2. Refine
102  const refined = await chat(
103    { session_id: sessionId, message: "make it harder, add core work" },
104    jwtToken
105  );
106  console.log(`Trainer: ${refined.message}\n${formatPlan(refined)}`);
107
108  // 3. Confirm — natural language, no magic string required
109  const confirmed = await chat(
110    { session_id: sessionId, message: "looks good, let's start" },
111    jwtToken
112  );
113  console.log(`Confirmed: ${confirmed.workout_plan?.action === "CONFIRMED"}`);
114  console.log(`Final plan:\n${formatPlan(confirmed)}`);
115  return confirmed;
116}