ringg_flutter 0.1.1
ringg_flutter: ^0.1.1 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.
//
// Offline by default (mock transport + mock backend, no secrets). Real mode:
// flutter run -d macos --dart-define-from-file=dart_defines.json
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 '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;
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)),
home: const HostPage(),
);
}
final class HostPage extends StatefulWidget {
const HostPage({super.key});
@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() => Harness.create(
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,
),
),
),
),
),
],
),
);
}
}