> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/ios/ja/navigation/v2/llms.txt)

# Feedback Agent

# Feedback Agent v2

> **Note (beta): Public Preview for Feedback Agent**
> 
> Feedback Agent is available in Public Preview. During this phase, it may be subject to potential breaking changes as the features and API surfaces stabilize.

Collect feedback and turn it into actionable insights. Feedback Agent helps you understand user needs faster and improve your product quickly — right inside your app.

> **Related content (guide): [MapGPT Feedback Agent v3 SDK](https://docs.mapbox.com/ios/ja/navigation/guides/mapgpt/)**
> 
> Looking for v3? Feedback Agent functionality is also available in public preview for v3 and compatible with Navigation SDK for iOS v3.

## How to Enable Feedback Agent

To enable Feedback Agent in Navigation SDK, follow these steps:

> **Related content (guide): [Navigation 'Get Started' Guide](https://docs.mapbox.com/ios/ja/navigation/v2/guides/get-started/)**
> 
> For new projects you may need to follow the Navigation "Get Started" guide to configure your Mapbox access token and project settings.

### Step 1: Add the dependency

Add the following dependencies to your Xcode Package Dependencies or Package.swift

```swift
dependencies: [
    .package(url: "https://github.com/mapbox/mapbox-navigation-ios.git", exact: 2.21.0),
    .package(url: "https://github.com/mapbox/mapbox-mapgpt-ios.git", exact: 2.20.1-alpha.1),
],
```

### Step 2: Set up an example view controller to use navigation

> **Related content (guide): [Turn-by-turn navigation guide](https://docs.mapbox.com/ios/ja/navigation/v2/guides/turn-by-turn-navigation/)**
> 
> To learn more about setting this up in your project follow the Navigation "Turn-by-turn navigation" guide.

> **Related content (guide): [Free-drive navigation guide](https://docs.mapbox.com/ios/ja/navigation/v2/guides/free-drive/)**
> 
> To learn more about setting this up in a new project follow the Navigation "Free-drive navigation" guide.

```swift
@_spi(ExperimentalMapboxAPI) import MapboxFeedbackAgent
@_spi(ExperimentalMapboxAPI) import MapboxFeedbackAgentUI
import MapboxCoreNavigation
import MapboxDirections
import MapboxMaps
import MapboxNavigation
import UIKit

var simulationIsEnabled = true

class ExampleViewController: UIViewController, NavigationMapViewDelegate, NavigationViewControllerDelegate {
    private let routingProvider = MapboxRoutingProvider()

    var navigationMapView: NavigationMapView! {
        didSet {
            if oldValue != nil {
                oldValue.removeFromSuperview()
            }

            navigationMapView.translatesAutoresizingMaskIntoConstraints = false

            view.insertSubview(navigationMapView, at: 0)

            NSLayoutConstraint.activate([
                navigationMapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                navigationMapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                navigationMapView.topAnchor.constraint(equalTo: view.topAnchor),
                navigationMapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            ])
        }
    }

    var indexedRouteResponse: IndexedRouteResponse? {
        didSet {
            guard indexedRouteResponse?.currentRoute != nil else {
                navigationMapView.removeRoutes()
                navigationMapView.removeWaypoints()
                return
            }
            showCurrentRoute()
        }
    }

    var currentRoute: Route? {
        return indexedRouteResponse?.currentRoute
    }

    var routes: [Route]? {
        return indexedRouteResponse?.routeResponse.routes
    }

    func showCurrentRoute() {
        guard let currentRoute = currentRoute else { return }

        var routes = [currentRoute]
        routes.append(
            contentsOf: self.routes!.filter {
                $0 != currentRoute
            }
        )
        navigationMapView.showcase(routes)
        navigationMapView.showRouteDurations(along: routes)
    }

    var startButton = UIButton()
    let voiceFeedbackButton: FloatingButton = {
        let image = UIImage(named: "icon_feedback_agent", in: .feedbackAgentUI, compatibleWith: nil)!
        let button = FloatingButton.rounded(image: image.withRenderingMode(.alwaysTemplate))
        button.accessibilityLabel = "Voice Feedback"
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = #colorLiteral(red: 0.921431005, green: 0.9214526415, blue: 0.9214410186, alpha: 1)
        button.layer.shadowColor = UIColor.black.cgColor
        button.layer.shadowOpacity = 0.3
        button.layer.shadowOffset = CGSize(width: 4, height: 4)
        return button
    }()

    let navigationLocationManager: NavigationLocationManager
    var passiveLocationManager: PassiveLocationManager?
    var passiveLocationProvider: PassiveLocationProvider?

    // MARK: - UIViewController lifecycle methods

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        navigationLocationManager = NavigationLocationManager()
        passiveLocationManager = PassiveLocationManager(systemLocationManager: navigationLocationManager)
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        // Use `passiveLocationManager.pauseTripSession()` to avoid billing implications
        passiveLocationManager?.delegate = self
        if let passiveLocationManager {
            passiveLocationProvider = PassiveLocationProvider(locationManager: passiveLocationManager)
        }
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationMapView = NavigationMapView(frame: view.bounds)
        navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        navigationMapView.delegate = self
        navigationMapView.userLocationStyle = .puck2D()
        if let passiveLocationProvider {
            navigationMapView.mapView.location.overrideLocationProvider(with: passiveLocationProvider)
        }

        let navigationViewportDataSource = NavigationViewportDataSource(
            navigationMapView.mapView,
            viewportDataSourceType: .raw
        )
        navigationViewportDataSource.options.followingCameraOptions.zoomUpdatesAllowed = false
        navigationViewportDataSource.followingMobileCamera.zoom = 13.0
        navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource

        let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        navigationMapView.addGestureRecognizer(gesture)

        view.addSubview(navigationMapView)

        startButton.setTitle("Start Navigation", for: .normal)
        startButton.translatesAutoresizingMaskIntoConstraints = false
        startButton.backgroundColor = .blue
        startButton.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
        startButton.addTarget(self, action: #selector(tappedButton(sender:)), for: .touchUpInside)
        startButton.isHidden = true
        view.addSubview(startButton)

        startButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive =
            true
        startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        view.setNeedsLayout()

        view.addSubview(voiceFeedbackButton)
        voiceFeedbackButton.translatesAutoresizingMaskIntoConstraints = false
        voiceFeedbackButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12).isActive =
            true
        voiceFeedbackButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12)
            .isActive = true
        voiceFeedbackButton.addTarget(self, action: #selector(passiveVoiceFeedback(_:)), for: .touchUpInside)
    }

    /// Override layout lifecycle callback to be able to style the start button.
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        startButton.layer.cornerRadius = startButton.bounds.midY
        startButton.clipsToBounds = true
        startButton.setNeedsDisplay()
    }

    @objc func tappedButton(sender _: UIButton) {
        guard let indexedRouteResponse else { return }

        // For demonstration purposes, simulate locations if the Simulate Navigation option is on.
        let navigationService = MapboxNavigationService(
            indexedRouteResponse: indexedRouteResponse,
            customRoutingProvider: routingProvider,
            credentials: NavigationSettings.shared.directions.credentials,
            locationSource: navigationLocationManager,
            simulating: simulationIsEnabled ? .always : .onPoorGPS
        )

        // Replace default `NavigationMapView` instance with instance that is used in preview mode.
        let navigationOptions = NavigationOptions(
            navigationService: navigationService,
            navigationMapView: navigationMapView
        )

        let navigationViewController = NavigationViewController(
            for: indexedRouteResponse,
            navigationOptions: navigationOptions
        )

        navigationViewController.delegate = self
        navigationViewController.modalPresentationStyle = .fullScreen

        if let latestValidLocation = navigationMapView.mapView.location.latestLocation?.location {
            navigationViewController.navigationMapView?.moveUserLocation(to: latestValidLocation)
        }

        startButton.isHidden = true

        let activeVoiceFeedbackButton: FloatingButton = {
            let image = UIImage(named: "icon_feedback_agent", in: .feedbackAgentUI, compatibleWith: nil)!
            let button = FloatingButton.rounded(image: image.withRenderingMode(.alwaysTemplate))
            button.accessibilityLabel = "Voice Feedback"
            button.translatesAutoresizingMaskIntoConstraints = false
            button.backgroundColor = .white
            return button
        }()

        activeVoiceFeedbackButton.addTarget(self, action: #selector(activeVoiceFeedback(_:)), for: .touchUpInside)

        navigationViewController.navigationView.floatingButtons?.append(
            activeVoiceFeedbackButton
        )

        present(navigationViewController, animated: true)
    }

    @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        guard gesture.state == .ended else { return }
        let location = navigationMapView.mapView.mapboxMap.coordinate(
            for: gesture.location(in: navigationMapView.mapView)
        )

        requestRoute(destination: location)
    }

    func requestRoute(destination: CLLocationCoordinate2D) {
        guard let userLocation = navigationMapView.mapView.location.latestLocation else { return }

        let location = CLLocation(
            latitude: userLocation.coordinate.latitude,
            longitude: userLocation.coordinate.longitude
        )

        let userWaypoint = Waypoint(
            location: location,
            heading: userLocation.heading,
            name: "user"
        )

        let destinationWaypoint = Waypoint(coordinate: destination)

        let navigationRouteOptions = NavigationRouteOptions(waypoints: [userWaypoint, destinationWaypoint])

        routingProvider.calculateRoutes(options: navigationRouteOptions) { [weak self] result in
            switch result {
            case let .failure(error):
                print(error.localizedDescription)
            case let .success(indexedRouteResponse):
                guard let self else { return }

                self.indexedRouteResponse = indexedRouteResponse
                self.startButton.isHidden = false
            }
        }
    }

    /// Delegate method called when the user selects a route
    func navigationMapView(_: NavigationMapView, didSelect route: Route) {
        guard let routes = indexedRouteResponse?.routeResponse.routes else { return }

        let currentRouteIndex = routes.firstIndex(of: route) ?? 0
        indexedRouteResponse?.routeIndex = currentRouteIndex
        showCurrentRoute()
    }

    func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling _: Bool) {
        let duration = 1.0
        navigationViewController.navigationView.topBannerContainerView.hide(duration: duration)
        navigationViewController.navigationView.bottomBannerContainerView.hide(
            duration: duration,
            animations: {
                navigationViewController.navigationView.wayNameView.alpha = 0.0
                navigationViewController.navigationView.floatingStackView.alpha = 0.0
                navigationViewController.navigationView.speedLimitView.alpha = 0.0
            },
            completion: { [weak self] _ in
                navigationViewController.dismiss(animated: false) {
                    guard let self = self else { return }

                    // Show previously hidden button that allows to start active navigation.
                    self.startButton.isHidden = false

                    // Since `NavigationViewController` assigns `NavigationMapView`'s delegate to itself,
                    // delegate should be re-assigned back to `NavigationMapView` that is used in preview mode.
                    self.navigationMapView.delegate = self

                    // Replace `NavigationMapView` instance with instance that was used in active navigation.
                    self.navigationMapView = navigationViewController.navigationMapView

                    // Since `NavigationViewController` uses `UserPuckCourseView` as a default style
                    // of the user location indicator - revert to back to default look in preview mode.
                    self.navigationMapView.userLocationStyle = .puck2D()

                    // Showcase originally requested routes.
                    if let routes = self.routes {
                        let cameraOptions = CameraOptions(bearing: 0.0, pitch: 0.0)
                        self.navigationMapView.showcase(
                            routes,
                            routesPresentationStyle: .all(shouldFit: true, cameraOptions: cameraOptions),
                            animated: true,
                            duration: duration
                        )
                    }
                }
            }
        )
    }
}

extension ExampleViewController {
    @objc func passiveVoiceFeedback(_: Any) {
        guard let passiveLocationManager else {
            NSLog(
                "\(self) \(#function) Cannot present VoiceFeedbackViewController from passive navigation flow when passiveLocationManager is nil"
            )
            return
        }
        let voiceFeedbackViewController = VoiceFeedbackViewController(passiveLocationManager: passiveLocationManager)
        present(voiceFeedbackViewController, animated: true)
        // Clean-up will occur after VoiceFeedbackViewController is dismissed and deallocated.
        navigationMapView.mapView.location.addLocationConsumer(newConsumer: voiceFeedbackViewController)
    }

    @objc func activeVoiceFeedback(_: Any) {
        guard let presentedNavController = presentedViewController as? NavigationViewController else {
            NSLog("\(self) \(#function) Active navigation is not presented, behavior mismatch")
            return
        }

        let voiceFeedbackViewController = VoiceFeedbackViewController(
            activeEventsManager: presentedNavController.navigationService.eventsManager
        )

        let locationManager = presentedNavController.navigationService.locationManager
        if let location = locationManager.location {
            voiceFeedbackViewController.locationUpdate(activeLocation: location)
            presentedNavController.present(voiceFeedbackViewController, animated: true)
        } else {
            NSLog("\(self) \(#function) Failed to provide VoiceFeedbackViewController with a location context")
        }
    }
}

extension ExampleViewController: PassiveLocationManagerDelegate {
    func passiveLocationManagerDidChangeAuthorization(_: MapboxCoreNavigation.PassiveLocationManager) {
        NSLog("\(self) \(#function)")
    }

    func passiveLocationManager(
        _: MapboxCoreNavigation.PassiveLocationManager,
        didUpdateLocation location: CLLocation,
        rawLocation _: CLLocation
    ) {
        NSLog("\(self) \(#function) \(location)")
    }

    func passiveLocationManager(_: MapboxCoreNavigation.PassiveLocationManager, didUpdateHeading newHeading: CLHeading)
    {
        NSLog("\(self) \(#function) \(newHeading)")
    }

    func passiveLocationManager(_: MapboxCoreNavigation.PassiveLocationManager, didFailWithError error: any Error) {
        NSLog("\(self) \(#function) \(error)")
    }
}
```

### Step 3: Set up an example FeedbackAgentViewController UI to access the agent

```swift
import AVFoundation
import CoreLocation
@_spi(ExperimentalMapboxAPI) import MapboxFeedbackAgent
import MapboxCommon
import MapboxCoreNavigation
import MapboxMaps
import UIKit

class VoiceFeedbackViewController: UIViewController, LocationConsumer {
    weak var passiveLocationManager: PassiveLocationManager?
    let feedbackAgentSession: FeedbackAgentSession

    /// Tap to request location permission (required before starting session)
    private var locationButton = UIButton(type: .custom)
    var resultLabel = UILabel()
    var stateLabel = UILabel()
    var transcriptTextView = UITextView()

    /// Development: custom-built UI to utilize FeedbackAgentSession
    var microphoneButton = UIButton(type: .custom)
    var horizontalMicrophoneStack = UIStackView()

    // MARK: - Public Functions

    /// Designated initializer to create a VoiceFeedbackViewController (which uses programmatic layouts) in free-drive
    /// navigation mode.
    /// - Parameter passiveLocationManager: A PassiveLocationManager instance which will be used to complete feedback
    /// telemetry and query the user's location.
    init(passiveLocationManager: PassiveLocationManager) {
        feedbackAgentSession = FeedbackAgentSession(
            passiveEventsManager: passiveLocationManager.eventsManager,
            options: .feedbackASR
        )
        self.passiveLocationManager = passiveLocationManager
        super.init(nibName: nil, bundle: nil)
        feedbackAgentSession.delegate = self
    }

    /// Designated initializer to create a VoiceFeedbackViewController (which uses programmatic layouts) in turn-by-turn
    /// navigation mode.
    /// - Parameter activeEventsManager: A NavigationEventsManager instance provided by a NavigationViewController's
    /// NavigationService which will be used to complete feedback telemetry and query the user's location.
    init(activeEventsManager: NavigationEventsManager) {
        feedbackAgentSession = FeedbackAgentSession(activeEventsManager: activeEventsManager, options: .feedbackASR)
        passiveLocationManager = nil
        super.init(nibName: nil, bundle: nil)
        feedbackAgentSession.delegate = self
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        if #available(iOS 13, *) {
            view.backgroundColor = UIColor.systemBackground
        } else {
            view.backgroundColor = UIColor.white
        }

        setupInterface()
        updateButtonState()
        if let uuid = feedbackAgentSession.options.uuid {
            resultLabel.text = "Session ID: \(uuid)"
        }
    }

    /// User-interface entry point
    @objc func startSession() {
        feedbackAgentSession.autoConnect(autoRecord: true)
    }

    @objc func requestLocation() {
        if let passiveLocationManager {
            passiveLocationManager.systemLocationManager.requestWhenInUseAuthorization()
            passiveLocationManager.systemLocationManager.startUpdatingLocation()
        }
    }

    /// Receive location updates from `MapboxMaps.LocationConsumer` during passive navigation.
    /// - Parameter newLocation: The newest known device location.
    func locationUpdate(newLocation: MapboxMaps.Location) {
        feedbackAgentSession.userContext = Context(location: newLocation.location)
        updateButtonState()
    }

    /// Receive location updates from ``ExampleViewController`` during active navigation.
    /// - Parameter activeLocation: The newest known device location.
    func locationUpdate(activeLocation: CLLocation) {
        feedbackAgentSession.userContext = Context(location: activeLocation)
        updateButtonState()
    }

    func updateButtonState() {
        microphoneButton.isEnabled = feedbackAgentSession.ready

        if feedbackAgentSession.ready {
            microphoneButton.backgroundColor = .green
        } else {
            microphoneButton.backgroundColor = .gray
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        stateLabel.text = feedbackAgentSession.state.display
    }

    // MARK: - Private Functions

    private func setupInterface() {
        microphoneButton.addTarget(self, action: #selector(startSession), for: .touchUpInside)
        microphoneButton.setTitle("Start Session", for: .normal)

        locationButton.addTarget(self, action: #selector(requestLocation), for: .touchUpInside)
        locationButton.setTitle("Request Location", for: .normal)
        locationButton.setTitleColor(.systemBlue, for: .normal)

        horizontalMicrophoneStack.axis = .horizontal
        horizontalMicrophoneStack.alignment = .fill
        horizontalMicrophoneStack.distribution = .equalCentering
        horizontalMicrophoneStack.addArrangedSubview(microphoneButton)
        horizontalMicrophoneStack.setContentHuggingPriority(.required, for: .vertical)

        transcriptTextView.setContentHuggingPriority(.defaultLow, for: .vertical)
        transcriptTextView.isEditable = false
        transcriptTextView.isScrollEnabled = true
        transcriptTextView.alwaysBounceVertical = true

        for child in [locationButton, resultLabel, stateLabel, horizontalMicrophoneStack, transcriptTextView] {
            view.addSubview(child)
            child.translatesAutoresizingMaskIntoConstraints = false
        }
        for label in [resultLabel, stateLabel] {
            label.text = "-"
            label.numberOfLines = 0
        }

        let margin: CGFloat = 8
        NSLayoutConstraint.activate([
            locationButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: margin),
            locationButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),

            resultLabel.topAnchor.constraint(equalTo: locationButton.bottomAnchor, constant: margin),
            resultLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            resultLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),

            stateLabel.topAnchor.constraint(equalTo: resultLabel.bottomAnchor, constant: margin),
            stateLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            stateLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),

            horizontalMicrophoneStack.topAnchor.constraint(equalTo: stateLabel.bottomAnchor, constant: margin),
            horizontalMicrophoneStack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            horizontalMicrophoneStack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            horizontalMicrophoneStack.heightAnchor.constraint(equalToConstant: 44),

            transcriptTextView.topAnchor.constraint(equalTo: horizontalMicrophoneStack.bottomAnchor, constant: margin),
            transcriptTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            transcriptTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            transcriptTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])

        microphoneButton.setNeedsLayout()
    }
}

extension VoiceFeedbackViewController: FeedbackAgentSessionDelegate {
    func feedbackAgent(session: FeedbackAgentSession, didReceiveTranscript _: String, isFinal _: Bool) {
        DispatchQueue.main.async { [weak transcriptTextView] in
            transcriptTextView?.text = session.transcriptContents.joined(separator: "\n")
        }
    }

    func feedbackAgent(session _: FeedbackAgentSession, didChangeState state: FeedbackAgentSessionState) {
        stateLabel.text = state.display
        updateButtonState()
    }

    /// FeedbackAgent end-result completion delegate function.
    /// May be invoked multiple times in the same session.
    /// Alternative: Use `FeedbackAgentSessionDelegate.feedbackAgent(_:didChangeState:)` and switch on `state` case .completed.
    /// - Parameters:
    ///   - session: The FeedbackAgentSession instance that reached an end state for this report. May be used again for subsequent reports.
    ///   - result: The session result: Success containing the UUID for the submitted report item, or an error if the report failed and was not submitted.
    func feedbackAgent(session _: FeedbackAgentSession, didComplete result: Result<UUID, any Error>) {
        switch result {
        case let .success(success):
            resultLabel.text = "Success: \(success)"
        case let .failure(failure):
            resultLabel.text = "Failure: \(failure)"
        }
    }
}
```

### Step 4: Get the Feedback ID from an item

The Feedback SDK automatically generates a feedback ID for every feedback item. You can optionally track this value to close the feedback loop with your users and notify them when one of their items has been closed, for example. For more information, see [Closing the Feedback Loop](#closing-the-feedback-loop).

```swift
extension VoiceFeedbackViewController: FeedbackAgentSessionDelegate {
    /// FeedbackAgent end-result completion delegate function.
    /// May be invoked multiple times in the same session.
    /// Alternative: Use `FeedbackAgentSessionDelegate.feedbackAgent(_:didChangeState:)` and switch on `state` case .completed.
    /// - Parameters:
    ///   - session: The FeedbackAgentSession instance that reached an end state for this report. May be used again for subsequent reports.
    ///   - result: The session result: Success containing the UUID for the submitted report item, or an error if the report failed and was not submitted.
    func feedbackAgent(session _: FeedbackAgentSession, didComplete result: Result<UUID, any Error>) {
        switch result {
        case let .success(success):
            print("Feedback report identifier is \(success.uuidString)")
            resultLabel.text = "Success: \(success)"
        case let .failure(failure):
            resultLabel.text = "Failure: \(failure)"
        }
    }
}
```

### Step 5: Add a microphone usage description

Your Info.plist will need to have a `NSMicrophoneUsageDescription` key value pair to explain all the ways that your application uses the microphone including for Feedback Agent.

## How to Send Test Items

When testing the Feedback Agent, it's important to understand how test items are handled to make sure they're properly identified and don't interfere with real feedback. Only production items are reviewed and validated by the Mapbox team. When using any production endpoint for testing, test reports are identified based on the content of the feedback:

-   If your feedback contains phrases like "test feedback" or "test report" (case-insensitive), the system will prefix your feedback with "This is a test feedback."
-   The feedback will still be categorized normally based on its content

```
Example:

User feedback: "I'm sending a test report about a road closure in front of me"

Classification: `road_closure_issue`

Feedback Description: "This is a test feedback. I'm sending a test report about a road closure in front of me"
```

## Feedback Explorer

The [Feedback Explorer](https://console.mapbox.com/feedback-explorer/) inside the Mapbox Console is the easiest mechanism to visualize the items from the users in your application:

![Mapbox Feedback Explorer Items List](https://docs.mapbox.com/ios/ja/assets/ideal-img/navigation-overview-feedback-explorer-list.91eebb4.480.png)

Tapping on a feedback item will give you all the details, including a map with the location of the report:

![Mapbox Feedback Explorer Item](https://docs.mapbox.com/ios/ja/assets/ideal-img/navigation-overview-feedback-explorer.4683560.480.jpg)

The Feedback Explorer allows you to:

-   Browse all feedback items from users of your application.
-   View the category generated by the Feedback Agent (see list below), the date of the item, the transcription, the resolution state, and the location of the item.
-   Filter items by date, category, or status (one of `open`, `acknowledged`, `fixed`, `unreproducible`, `unactionable`, `unsupported`).
-   Tap on an individual item to get all its information, including a map of the location of the item.

### Default Categories

By default, Feedback Agent provides a set of default categories for location-related items:

| Category | Description |
| --- | --- |
| `alternative_route_issue` | To report wrong or missing alternative route. |
| `banner_issue` | Problems with the given banners. |
| `eta_issue` | Problems with ETA quality. |
| `false_negative_traffic_issue` | When a user reports traffic congestion or delays (e.g., traffic jams or traffic build up) |
| `false_positive_traffic_issue` | When a user reports traffic is flowing smoothly and there's no congestion |
| `guidance_issue` | Problems with the given guidance. |
| `illegal_entrance` | When a suggested entrance is illegal. |
| `illegal_turn` | When a suggested turn is illegal. |
| `lane_guidance_issue` | Issues specifically around lane guidance or details about the road as displayed on the map. |
| `map_data_issue` | Problems with map features and functionality. |
| `map_rendering_issue` | Problems with map rendering. |
| `missing_expected_maneuver` | When a legal turn is not suggested. |
| `no_route` | When a route is not built. |
| `poi_closed` | When a user reports that a place on the map or navigation no longer exists. |
| `poi_details` | When a user reports that details about a place are incorrect or need to be updated. |
| `poi_location` | When a user reports that location about a place is incorrect. |
| `poi_missing` | Problems with the assistant's ability to find POIs, addresses, or other places. |
| `police_speed_trap` | When a user reports a police speed checkpoint. |
| `poor_arrival_issue` | To report issues with arrival behavior. |
| `poor_routing_issue` | Problems with the given route. |
| `positioning_issue` | Problems with a puck/location of car and matching to the current roads. |
| `rerouting_recalculation` | To report continuous or incorrect rerouting/route recalculation. |
| `road_closure_issue` | When a road is closed temporarily or permanently. |
| `road_incident_issue` | Problems with road incident. |
| `speed_limit_issue` | To report wrong or missing speed limit data. |
| `traffic_issue` | When the user wants to report traffic, congestion or incidents like trash in the road. |
| `voice_guidance_issue` | Problems with the given voice guidance. |
| `application_issue` | Capture issues with application performance, bugs, or errors unrelated to specific navigation features. |
| `non_feedback` | Irrelevant or nonsensical feedback that does not pertain to application functionality or navigation issues. |

Mapbox will automatically triage and act on these items on your behalf to improve the underlying data based on items from users of your application.

### Custom Categories

Besides the default categories mentioned above, Feedback Agent provides a generic `application_issue` category to capture issues with application performance, bugs, or errors unrelated to specific navigation features.

If you need more fine-grained application-related categories, you can also define your own. For example, a food delivery application might want to collect specific information about `food_packaging`.

For more information on how to create custom categories, see [contact us](https://www.mapbox.com/forms/voice-feedback-agent).

## Closing the Feedback Loop

When using Feedback Agent you can close the feedback loop with the user that submitted an item, by notifying them that their feedback has been received and its current status. At a high level, the process will be as follows:

1.  Get the `feedbackId` from the Feedback SDK. This is a unique identifier string automatically generated by the Feedback SDK.
2.  Store this value in your system associated with a specific logged-in user in your app.
3.  Use the [Mapbox Feedback API](#feedback-api) to query the status of the item using its `feedbackId`.
4.  When the status changes, use a dedicated screen, a push notification, email, or any other relevant mechanism to notify the user.

## Feedback API

The [Mapbox Feedback API](https://docs.mapbox.com/api/feedback/) provides a unified, programmatic way to access end user feedback collected from the apps you build with Mapbox.

This API allows you to integrate feedback data directly into your own internal tools and data pipelines, giving you visibility into the issues and suggestions your users provide. You can track the status of feedback items as they are reviewed by Mapbox and get valuable insights about your applications.

In particular, the Feedback API allows querying items by `feedbackId` to close the feedback loop with your users.

## Language Support

During the Public Preview, Feedback Agent supports the following languages:

-   English (en-US)
-   German (de-DE)
-   Spanish (es-ES)
-   Japanese (ja-JP)

Other languages and locales are accepted, but recognition accuracy and classification quality may vary. If your use case requires support for languages not in this list, [contact us](https://www.mapbox.com/contact).

## Pricing Information

Feedback Agent is free during the Public Preview, including access to Feedback Explorer and the Feedback API.

At General Availability, customers using Feedback Agent will incur a charge of $0.02 per feedback item, with data retained only for a limited period. This charge includes access to Feedback Explorer and the Feedback API.

For custom categories, contractual service level agreements (SLAs) on map and navigation feedback resolution, or advanced reporting, [contact us](https://www.mapbox.com/contact).