ringg_flutter 0.1.5 copy "ringg_flutter: ^0.1.5" to clipboard
ringg_flutter: ^0.1.5 copied to clipboard

Ringg AI widget for Flutter — drop-in widget and headless controller for voice/text AI agents.

example/lib/main.dart

// Example host page — the Flutter cut of the web example's App.tsx.
//
// The page itself stays bare (the web example's empty index.html body): a
// plain white Scaffold whose only tenants are the RinggWidget under test and
// the floating dev config panel. The panel's config edits recreate the
// harness (controller + transport), exactly like the web example — the
// conversation resets with it.
//
// 100% REAL: needs credentials via --dart-define (no mock/echo path). Run:
//   flutter run -d macos --dart-define-from-file=dart_defines.json
// Without credentials the app shows a "set your credentials" screen.

import 'package:flutter/material.dart';
import 'package:ringg_flutter/ringg_flutter.dart';

import 'dev_panel/dev_config_panel.dart';
import 'dev_panel/dev_overrides.dart';
import 'dev_panel/event_log.dart';
import 'env.dart';
import 'harness.dart';
import 'probe.dart';

/// Flip to true to boot the headless-core dev probe (probe.dart) instead of
/// the styled widget — core debugging without the UI in the way.
const bool kUseProbe = false;

/// Builds the [Harness] for the host page. The app uses [Harness.create]
/// (real livekit transport + backend); tests inject a factory that supplies an
/// in-memory transport — the only way a mock enters the tree.
typedef HarnessFactory = Harness Function({
  required RinggWidgetConfig config,
  required EventBus eventBus,
  required DomActionExecutor onDomAction,
});

void main() => runApp(kUseProbe ? const ProbeApp() : const ExampleApp());

final class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Ringg Widget Example',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(colorSchemeSeed: const Color(0xFF0A0A0B)),
        // Real-only: no credentials → nothing to connect to.
        home: Env.hasRealCreds
            ? const HostPage()
            : const _CredentialsRequired(),
      );
}

/// Shown when the app is launched without real credentials. The harness is
/// real-only, so there is nothing to run offline.
final class _CredentialsRequired extends StatelessWidget {
  const _CredentialsRequired();

  @override
  Widget build(BuildContext context) => const Scaffold(
        backgroundColor: Colors.white,
        body: Center(
          child: Padding(
            padding: EdgeInsets.all(32),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('Credentials required',
                    style:
                        TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
                SizedBox(height: 12),
                Text(
                  'This example talks to the real Ringg backend. Copy\n'
                  'dart_defines.json.example → dart_defines.json, fill your\n'
                  'AGENT_ID + X_API_KEY, then run:\n\n'
                  'flutter run --dart-define-from-file=dart_defines.json',
                  textAlign: TextAlign.center,
                  style: TextStyle(height: 1.5, color: Color(0xFF52525B)),
                ),
              ],
            ),
          ),
        ),
      );
}

final class HostPage extends StatefulWidget {
  const HostPage({super.key, this.harnessFactory});

  /// Test seam: build the harness with an injected (mock) transport instead of
  /// the real livekit one. Null in the app → real [Harness.create].
  final HarnessFactory? harnessFactory;

  @override
  State<HostPage> createState() => _HostPageState();
}

final class _HostPageState extends State<HostPage> {
  // The "window listener" of this harness — host events and dom-action
  // dispatches land here; the panel's Events tab renders it.
  final DevEventLog _eventLog = DevEventLog();

  DevOverrides _overrides = DevOverrides();
  late Harness _harness = _createHarness();

  Harness _createHarness() {
    final factory = widget.harnessFactory ?? Harness.create;
    return factory(
      config: _overrides.apply(baseConfig()),
      eventBus: createLoggingEventBus(_eventLog),
      onDomAction: (action, log) =>
          executeDevDomAction(action, _eventLog, log: log),
    );
  }

  /// Web parity (App.tsx onConfigChange): swap in a fresh harness for the new
  /// config; the old one is disposed after the frame so the outgoing widget
  /// subtree never touches a destroyed controller.
  void _applyOverrides(DevOverrides next) {
    final old = _harness;
    setState(() {
      _overrides = next;
      _harness = _createHarness();
    });
    WidgetsBinding.instance.addPostFrameCallback((_) => old.dispose());
  }

  @override
  void dispose() {
    _harness.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final controller = _harness.controller;
    return Scaffold(
      backgroundColor: Colors.white,
      body: Stack(
        children: [
          // The widget under test fills the page — it positions its trigger
          // and panel within this box (the web's fixed positioning).
          // ObjectKey remounts the subtree per harness so no view carries
          // state across a config rebuild.
          Positioned.fill(
            child: RinggWidget(
              key: ObjectKey(controller),
              controller: controller,
              transport: _harness.livekit,
            ),
          ),
          // Dev chrome on top. isCalling mirrors the web example's
          // `session.connectionState === "connected"` gate. SafeArea keeps
          // the gear/panel clear of the home indicator + status bar on mobile.
          Positioned.fill(
            child: SafeArea(
              child: StoreBuilder<ShellSnapshot>(
                store: controller.shell,
                builder: (context, shell, _) => StoreBuilder<SessionSnapshot>(
                  store: controller.session,
                  builder: (context, session, _) => DevConfigPanel(
                    widgetOpen: shell.viewState != WidgetViewState.closed,
                    overrides: _overrides,
                    baseConfig: baseConfig(),
                    onOverridesChanged: _applyOverrides,
                    // The exact call the web dev panel makes: raw wire payload →
                    // passthrough component → the full typed-parse + buffering
                    // path, no backend needed.
                    onInjectComponent: (payload) => controller.components.add(
                      PassthroughComponentPayload(payload),
                      controller.shell.snapshot.callMode,
                    ),
                    onOpenWidget: controller.openWidget,
                    onFireDomAction: (action) =>
                        executeDevDomAction(action, _eventLog),
                    eventLog: _eventLog,
                    isCalling: session.connectionState ==
                        TransportConnectionState.connected,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
140
points
113
downloads

Documentation

API reference

Publisher

verified publisherringg.ai

Weekly Downloads

Ringg AI widget for Flutter — drop-in widget and headless controller for voice/text AI agents.

Homepage

License

MIT (license)

Dependencies

audioplayers, flutter, flutter_markdown_plus, flutter_svg, http, livekit_client, markdown, package_info_plus, url_launcher

More

Packages that depend on ringg_flutter