Graine AI

The voice bar

Talk to the agent inside your app: the bar's launcher, the call it places, captions, mute and end, where it sits and how it follows the keyboard, your own button that starts a call, and what to listen for — on every platform.

The bar is the agent's voice inside your app: tap, talk, see captions, mute, end — in your design, carried across screens, rising above the keyboard. It is not a phone-style call (that is the call engine): it lives while your app is open, and a conversation ends when the app goes to the background.

The assistant bar open and listening on an iPhone, under the agent's first line, beside the website widget in the same design

What it needs in the dashboard

WhereSettingValue
Embed & Widgets → Appearance → SetupWhat visitors getCall, or Call and chat
Embed & Widgets → Appearance → SetupCall audioWebRTC (recommended) or Standard — how the bar's voice travels
The Garage → the whole lookAccent, shape, glyph, typeface, motionHow the bar looks — The assistant bar

What to do, in order

  1. Configure the SDK and put the bar on screen (install).
  2. The user taps the launcher. → The SDK asks for the microphone the first time, places the call and says what the network is doing (Connecting, Still connecting…, Reconnecting).
  3. They talk. → Captions show the line being said, either side; the meter moves with the agent's voice.
  4. Start or end it from your own button, mute it, or read its state, if your screens need to.
  5. Listen for the call ending, the microphone being refused, and errors.
  6. It ends with the end button, your end(), or the app going to the background (two seconds later, unless you turned that off).

The code, platform by platform

1. The bar, draggable, its place kept by you:

struct RootView: View {
    @AppStorage("barOffset") private var barOffset: Double = 0
    var body: some View {
        Text("Your app")
            .overlay(GraineAgentBar(
                initialOffset: CGFloat(barOffset),
                onPlacementChange: { barOffset = Double($0) },      // where the user let go; the SDK stores nothing
                onClose: { print("End pressed with no call") }
            ))
    }
}

4. Your own button, and the state to draw from — GraineVoice is an ObservableObject:

struct TalkButton: View {
    @ObservedObject var voice = GraineInApp.shared.voice
    var body: some View {
        Button(voice.connected ? "End" : "Talk to us") {
            voice.connected ? voice.end() : voice.start()
        }
        .disabled(voice.connecting)
    }
}

→ phase is one thing to draw from: idle, connecting, reconnecting, thinking, speaking, listening. captions holds the transcript; setMuted(true) withholds the user's voice.

5. Listen:

final class VoiceEvents: GraineListener {
    func onCaption(role: GraineRole, text: String, live: Bool) { if !live { print("\(role): \(text)") } }
    func onMicDenied(reason: String) { print("Microphone refused: \(reason)") }
    func onEnded() { print("The conversation ended") }
}

6. Keep a call going in the background: GraineConfig(…, endOnBackground: false) — and the Audio background mode.

Troubleshooting

What you seeWhyFix
Needs microphoneNot declared, or refusedPermissions
Still connecting… for longA slow network; the bar keeps tryingIt says Couldn't connect with Try again when nothing can carry the call
The call ends when the app is backgroundedendOnBackground (on by default)Turn it off, with the Audio background mode on iOS — or use phone-style calls
The agent cannot hear the user on a call while the chat is openOne conversation at a timeOpening the chat ends the call, and back

In depth: Voice · SDK reference → Voice.

On this page