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:
| Platform | Minimum SDK |
| Swift (iOS) | KinesteXAIKit ≥ 1.1.3 — iOS 13+, Swift 5.5+, SwiftUI. ≥ 1.1.4 for subscription gating (adds the workoutAction binding) |
| Kotlin (Android) | KinesteX-SDK-Kotlin ≥ 2.0.6 |
| React Native | kinestex-sdk-react-native ≥ 1.2.9 |
| Flutter | kinestex_sdk_flutter ≥ 1.4.7 |
| React TS (Web) | kinestex-sdk-react-ts ≥ 0.0.3 |
| HTML & JS | iframe 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.
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.
| Field | Unit / Allowed values |
age | years |
height | cm (UI lets users switch to imperial after) |
weight | kg |
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:
| Field | Type | Notes | |||
fitness_goals | string[] | "weight_loss", "muscle_gain", "strength", "general_fitness", "wellness_flexibility", "cardio_endurance" | |||
injuries | object[] | `{ "body_part", "preference": "avoid" \ | "include", "severity": "light" \ | "moderate" \ | "severe" }` |
health_conditions | string[] | e.g. ["Asthma"] | |||
fitness_level_squats / _pushups / _cardio | string | Descriptive levels; the in-app assessment sets the first two automatically | |||
other_preferences | string | Free 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.
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:
| Key | Type | Effect |
aiTrainerName | string | Renames the AI trainer everywhere it's labelled — the header title and the label above each reply. Shown verbatim in every language. Example: "My Coach" |
aiTrainerColor | string | Recolors 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.
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
isSubscribedincustomParamson every mount. Missing ortrue= subscribed; only an explicitfalsegates 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: theworkoutActionbinding, ≥ 1.1.4; Kotlin:sendAction). On"purchased"the parked generation resumes automatically — no second tap. - Standalone/link integrations with no host app can pass
subscriptionReturnUrlinstead — 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):
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:
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:
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.
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; otherwiseuser: nil.Handle
trainer_schedule_next_workout(arrives as.custom_type) and schedule a reminder viaUNUserNotificationCenter(or push).Use a stable identifier (e.g.
"kinestex.trainer.next_workout") so re-schedules replace the previous reminder.NSCameraUsageDescriptionset inInfo.plistand notification permission requested early.