ringg_flutter 0.1.4
ringg_flutter: ^0.1.4 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,
),
),
),
),
),
],
),
);
}
}