KinesteX

AI Trainer Chat

Overview

The AI Personal Trainer is a guided chat experience that walks the user through a personalized session: profile setup (with an optional camera-based fitness assessment) → readiness check-in → workout → post-workout check-in → scheduling the next session.


Your app only needs to mount the trainer view, optionally pre-fill user data, and listen for the events below — most importantly trainer_schedule_next_workout, which fires when the user picks a date/time for their next session and is the trigger for your reminder logic. Optionally, you can also gate workout generation behind your subscription flow and keep the fitness profile in sync with your app.


Prefer to build your own chat UI? The same intelligence is available over REST — see the AI Trainer API.

Requirements

Camera permission, an internet connection, and a recent SDK that supports the Trainer Chat integration:


PlatformMinimum SDK
Swift (iOS)KinesteXAIKit1.1.3 — iOS 13+, Swift 5.5+, SwiftUI. ≥ 1.1.4 for subscription gating (adds the workoutAction binding)
Kotlin (Android)KinesteX-SDK-Kotlin2.0.6
React Nativekinestex-sdk-react-native1.2.9
Flutterkinestex_sdk_flutter1.4.7
React TS (Web)kinestex-sdk-react-ts0.0.3
HTML & JSiframe at https://ai.kinestex.com/trainer (HTTPS host page required)

If you haven't installed the SDK yet, follow the installation guide first — only trainer-specific concerns are covered below.

Launching the Trainer

Mount the trainer with the AI_TRAINER_CHAT integration option (or, on Swift / Kotlin / Flutter, call createTrainerChatView). The trainer flow is rendered entirely inside the SDK.

Swift example:
1import SwiftUI
2import KinesteXAIKit
3
4struct TrainerScreen: View {
5    @State private var isLoading = false
6    @State private var showKinesteX = true
7
8    // KinesteXAIKit is configured per-instance — no global initialize call.
9    private let kit = KinesteXAIKit(
10        apiKey: "<YOUR_API_KEY>",
11        companyName: "<YOUR_COMPANY>",
12        userId: "<USER_ID>"
13    )
14
15    var body: some View {
16        if showKinesteX {
17            kit.createTrainerChatView(
18                user: nil,                       // or your UserDetails — see below
19                style: IStyle(style: "dark"),
20                isLoading: $isLoading,
21                onMessageReceived: handleMessage
22            )
23        }
24    }
25
26    private func handleMessage(_ message: KinestexMessage) { /* see below */ }
27}

Pre-filling user data

The trainer asks profile questions on first use. You can skip the basic demographics by passing them when you mount the SDK — anything you omit, the trainer will simply ask for.


FieldUnit / Allowed values
ageyears
heightcm (UI lets users switch to imperial after)
weightkg
gender"male" or "female"

Don't pass empty strings or zero — omit the field instead. On Swift, Kotlin, and Flutter the UserDetails initializer requires all fields, so either pass the complete value or skip user: entirely (nil on Swift, omit on Kotlin/Flutter).


Fitness-profile prefill. Beyond demographics, you can pre-fill the trainer's fitness profile via customParams so returning users skip questions you already have answers for. The field names match the trainer_profile_updated event exactly:


FieldTypeNotes
fitness_goalsstring[]"weight_loss", "muscle_gain", "strength", "general_fitness", "wellness_flexibility", "cardio_endurance"
injuriesobject[]`{ "body_part", "preference": "avoid" \"include", "severity": "light" \"moderate" \"severe" }`
health_conditionsstring[]e.g. ["Asthma"]
fitness_level_squats / _pushups / _cardiostringDescriptive levels; the in-app assessment sets the first two automatically
other_preferencesstringFree text, passed to the AI verbatim

The trainer merges what you pass into the stored profile (your values win). Only send fields that hold real values — never placeholders, empty strings, or zeros.

Swift example:
1// UserDetails on Swift requires ALL five fields — pass the full value when
2// you know everything, otherwise pass `user: nil` and let the trainer ask.
3let user = UserDetails(
4    age: 32,
5    height: 178,         // cm
6    weight: 76,          // kg
7    gender: .Male,
8    lifestyle: .Active   // not used in the trainer UI, but required by the initializer
9)
10
11kit.createTrainerChatView(
12    user: user,
13    style: IStyle(style: "dark"),
14    isLoading: $isLoading,
15    onMessageReceived: handleMessage
16)

Branding & appearance

Two optional customParams rebrand the trainer without any KinesteX involvement:


KeyTypeEffect
aiTrainerNamestringRenames the AI trainer everywhere it's labelled — the header title and the label above each reply. Shown verbatim in every language. Example: "My Coach"
aiTrainerColorstringRecolors the trainer's star icon — any CSS color, e.g. "#7C3AED"

The overall theme (colors, fonts) is configured by KinesteX per company — send your KinesteX contact the palette you want, or manage it via your white-label theme.

Handling messages

The trainer emits the standard SDK events plus its own trainer-specific ones. All of them arrive as JSON with a snake_case type — on Swift/Kotlin/Flutter they arrive through the generic custom_type / CustomType case you already handle:


  • workout_exit_request — the user exited a workout that was launched from the trainer.
  • trainer_schedule_next_workout — the user picked a date/time for their next session or assessment (see below).
  • open_subscription_flow — a non-subscribed user tried to generate a workout (see Subscription gating).
  • trainer_assessment_started / trainer_assessment_completed / trainer_assessment_skipped — in-app fitness assessment lifecycle (see Assessment & profile sync).
  • trainer_profile_updated — the user's fitness profile changed; persist it on your side (see Assessment & profile sync).

Add the new cases to your existing handler.

Swift example:
1private func handleMessage(_ message: KinestexMessage) {
2    switch message {
3    case .exit_kinestex:
4        // User exited the trainer screen via the back button.
5        showKinesteX = false
6
7    case .error_occurred(let data):
8        print("KinesteX error:", data)
9
10    // `trainer_schedule_next_workout` is not yet a typed case — it
11    // arrives as .custom_type, so check the `type` field on the payload.
12    case .custom_type(let data):
13        guard let type = data["type"] as? String else { return }
14        switch type {
15        case "workout_exit_request":
16            // User exited a workout that was launched from the trainer.
17            break
18        case "trainer_schedule_next_workout":
19            // payload: { type, scheduledFor: "YYYY-MM-DDTHH:MM" }
20            if let scheduledFor = data["scheduledFor"] as? String {
21                scheduleNextWorkoutReminder(scheduledFor: scheduledFor)
22            }
23        default:
24            break
25        }
26
27    default:
28        break
29    }
30}

Subscription gating

Workout generation can be gated behind your app's subscription; browsing, onboarding, the free fitness assessment, and Q&A stay available to everyone. KinesteX never shows a paywall and never processes payments — your app owns the purchase flow.


  • Pass isSubscribed in customParams on every mount. Missing or true = subscribed; only an explicit false gates generation.
  • When a non-subscriber taps Generate workout, the SDK parks the request and emits open_subscription_flow — overlay your subscription screen on top of the trainer view.
  • When your flow closes, post { "subscription_result": "purchased" } or { "subscription_result": "dismissed" } back into the SDK (Swift: the workoutAction binding, ≥ 1.1.4; Kotlin: sendAction). On "purchased" the parked generation resumes automatically — no second tap.
  • Standalone/link integrations with no host app can pass subscriptionReturnUrl instead — the trainer redirects the browser there rather than posting the event.

Full walkthrough with per-platform code, edge cases, and a launch checklist: Subscription gating guide. To enforce the status server-side (so clients can't bypass it), see Session Auth & Managed Subscriptions.

Assessment & profile sync

In-app fitness assessment. During onboarding the trainer offers a guided, camera-based squat and push-up assessment (30 seconds each) instead of self-reporting fitness levels — results set the user's measured fitness level automatically. Users can also tap Set manually or schedule the assessment for later. The lifecycle is reported to your app (dates are DD MM YYYY HH:mm:ss in device-local time — informational, don't parse them for logic):


json
1{ "type": "trainer_assessment_started", "date": "09 07 2026 14:52:10" }
2{ "type": "trainer_assessment_skipped", "reason": "set_manually", "date": "09 07 2026 14:52:10" }
3{
4  "type": "trainer_assessment_completed",
5  "date": "09 07 2026 14:52:10",
6  "results": {
7    "squats":  { "reps": 21, "level": "intermediate" },
8    "pushups": { "reps": 14, "level": "intermediate" }
9  },
10  "fitnessLevel": "intermediate"
11}

level and fitnessLevel are "beginner", "intermediate", or "advanced".


Profile sync (KinesteX → your app). Every profile change — onboarding answers, assessment results, edits the user makes in chat ("my knee hurts", "I'm 70kg now") — fires trainer_profile_updated with the full current profile:


json
1{
2  "type": "trainer_profile_updated",
3  "source": "assessment",
4  "profile": {
5    "age": 29,
6    "height": 172,
7    "weight": 68,
8    "gender": "female",
9    "fitness_goals": ["weight_loss", "cardio_endurance"],
10    "fitness_level_squats": "I can do 21 squats in 30 seconds",
11    "fitness_level_pushups": "I can do 14 push ups in 30 seconds",
12    "fitness_level_cardio": "I can run 1.6km (1 mile) with walking breaks",
13    "injuries": [
14      { "body_part": "lower_back", "preference": "avoid", "severity": "moderate" }
15    ],
16    "health_conditions": [],
17    "other_preferences": "Prefers morning workouts",
18    "updated_at": "2026-07-06T14:52:10Z"
19  }
20}

source is "onboarding", "assessment", or "chat". Persist the whole profile object keyed by user and store updated_at — last write wins if both sides edit the same field. Empty arrays/strings are always included so you receive the complete current profile.


Your app → KinesteX. No runtime call is needed — pass current values the next time you mount the trainer: demographics via user: / UserDetails, everything else via customParams using the same field names as the event (see Pre-filling user data).

Scheduling the next workout

When the user schedules their next workout — or the in-app fitness assessment — for later, the SDK fires:


json
1{
2  "type": "trainer_schedule_next_workout",
3  "scheduledFor": "2026-04-29T14:30",
4  "sessionType": "workout"
5}

scheduledFor is `YYYY-MM-DDTHH:MM` with no timezone — interpret it as the device's local time (new Date(scheduledFor) in JS and DateTime.parse(scheduledFor) in Dart both do this correctly). The event is not sent if the user skips scheduling.


sessionType is "workout" or "assessment" — treat a missing value as "workout". Use it to word the reminder and to pick a stable notification identifier per session type so re-scheduling replaces instead of stacking.


The SDK does not schedule the reminder for you — it just tells you when. The host app delivers the reminder. Two options:


  • Local notification (recommended for native). No backend, works offline, fires even if the app is force-quit. Example below.
  • Server-side push. POST { userId, scheduledFor, timezone } to your backend and queue a push (APNs / FCM / web-push / OneSignal). Use this when the user has multiple devices, you want delivery analytics, or the platform has no reliable local-notification path (e.g. plain web).

Whichever you pick, use a stable identifier per user (e.g. "trainer-next-workout") so a re-schedule replaces the previous reminder instead of stacking, skip past timestamps, and request notification permission early.

Local-notification example
Swift example:
1import UserNotifications
2
3private let trainerReminderId = "kinestex.trainer.next_workout" // stable per user
4
5func scheduleNextWorkoutReminder(scheduledFor: String) {
6    // Parse "YYYY-MM-DDTHH:MM" as local time. Don't use ISO8601DateFormatter —
7    // it expects seconds and/or a timezone designator.
8    let formatter = DateFormatter()
9    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm"
10    formatter.timeZone = .current
11    formatter.locale = Locale(identifier: "en_US_POSIX")
12
13    guard let date = formatter.date(from: scheduledFor),
14          date.timeIntervalSinceNow > 5 else { return }
15
16    let center = UNUserNotificationCenter.current()
17    center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
18        guard granted else { return }
19
20        // Replace the previous reminder so re-schedules don't stack.
21        center.removePendingNotificationRequests(withIdentifiers: [trainerReminderId])
22
23        let content = UNMutableNotificationContent()
24        content.title = "Time for your workout"
25        content.body  = "Your AI Trainer has your next session ready."
26        content.sound = .default
27
28        let components = Calendar.current.dateComponents(
29            [.year, .month, .day, .hour, .minute], from: date
30        )
31        let trigger = UNCalendarNotificationTrigger(
32            dateMatching: components, repeats: false
33        )
34        center.add(UNNotificationRequest(
35            identifier: trainerReminderId,
36            content: content,
37            trigger: trigger
38        ))
39    }
40}

Quick checklist

  • KinesteXAIKit ≥ 1.1.3 installed via Swift Package Manager.

  • KinesteXAIKit(apiKey:companyName:userId:) instantiated where you mount the trainer.

  • Mount via kit.createTrainerChatView(...).

  • Pass a fully-populated UserDetails (age, height in cm, weight in kg, gender, lifestyle) when known; otherwise user: nil.

  • Handle trainer_schedule_next_workout (arrives as .custom_type) and schedule a reminder via UNUserNotificationCenter (or push).

  • Use a stable identifier (e.g. "kinestex.trainer.next_workout") so re-schedules replace the previous reminder.

  • NSCameraUsageDescription set in Info.plist and notification permission requested early.