Nx10 Logo
Docs

4. SAAQ Prompts

Subjective Action-Anchored Questionnaires for ground-truth model training.

To truly understand human emotion, the Large Feelings Model (LFM) requires "ground truth" labels to calibrate against its kinematic observations. The Nx10 Control Plane actively monitors your incoming telemetry and decides when it is optimal to ask the user a question.

When this happens, the backend sends a trigger down to the iOS SDK. There are two integration paths for handling these triggers: Nx10 Managed UI and Custom Native UI.

Coming Soon

Path A: Managed UI

We provide fully styleable, native UIViewController and SwiftUI view components. The SDK handles rendering and payload submission natively with zero UI boilerplate.

Available Now

Path B: Custom UI

You build the prompt entirely in your own UIView. The SDK simply passes you the question properties and a Swift closure to execute when the user submits their answer.

The Art of the Prompt (Best Practices)

  • Smart Interruptions: We recognize you cannot pop up a UI while a user is mid-way through a critical payment flow. If the trigger fires during a sensitive task, cache the request and display it the exact moment they reach a safe state.
  • Incentivize Answers: Label yield is a critical KPI. Users are much more likely to answer if they get a micro-reward. Granting loyalty points or unlocking an article upon completion drastically reduces "Prompt Fatigue".

Implementing Custom UI (Path B)

The SDK exposes a listener closure that provides a SaaQRequest object (containing the details of the prompt) and an escaping (SaaQResponse) -> Void completion handler.

The Data Contract

Currently, the system supports basic prompt types (type1 and type2). The data model is designed to easily expand to support dynamic copy and multi-step forms in future releases.

Nx10Core/Models/SaaQ.swift
public struct SaaQRequest {
    // Identifies the layout to show. Currently supports "type1" (e.g., slider) or "type2" (e.g., buttons)
    public let promptType: String 
    
    //[PLACEHOLDER: Future properties will be injected here]
    // public let questionText: String?
    // public let options:[String]?
}

public struct SaaQResponse {
    public let wasAnswered: Bool
    public let selectedValue: String?
    
    public init(wasAnswered: Bool, selectedValue: String? = nil) {
        self.wasAnswered = wasAnswered
        self.selectedValue = selectedValue
    }
}

The Implementation

SAAQCoordinator.swift
import UIKit
import Nx10Core

class SAAQCoordinator {
    
    // Cache the completion handler to call later
    private var pendingCompletion: ((SaaQResponse) -> Void)?

    func setupNx10Listeners() {
        // Register the listener with the SDK
        Nx10Core.shared.saaq.onPromptRequested = {[weak self] request, completion in
            self?.handleSaaQRequest(request, completion: completion)
        }
    }

    private func handleSaaQRequest(_ request: SaaQRequest, completion: @escaping (SaaQResponse) -> Void) {
        
        // 1. Smart Interruption Check
        if AppState.shared.isUserInCriticalFlow {
            // Defer showing the UI until the flow is complete
            self.deferPrompt(request: request, completion: completion)
            return 
        }

        showPrompt(request: request, completion: completion)
    }

    private func showPrompt(request: SaaQRequest, completion: @escaping (SaaQResponse) -> Void) {
        // 2. Cache the completion handler
        self.pendingCompletion = completion
        
        // 3. Read the properties to decide which view to present
        let vc = CustomSaaQViewController()
        
        if request.promptType == "type1" {
            vc.configureAsSlider()
        } else if request.promptType == "type2" {
            vc.configureAsButtonGrid()
        }
        
        // Present your custom UI
        UIApplication.shared.windows.first?.rootViewController?.present(vc, animated: true)
    }

    // --- Called by your CustomSaaQViewController when the user taps an option ---

    func userDidSelectAnswer(_ feelingValue: String) {
        // 4. Construct the answer
        let response = SaaQResponse(wasAnswered: true, selectedValue: feelingValue)
        
        // 5. Fire the closure to send data back to the LFM
        pendingCompletion?(response)
        pendingCompletion = nil
        
        RewardSystem.grantPoints(50) // Reward them!
    }

    func userDidDismiss() {
        // 4b. Construct an empty response if they skipped
        let response = SaaQResponse(wasAnswered: false)
        pendingCompletion?(response)
        pendingCompletion = nil
    }
}