kletso_flutter
Embeddable Kletso AI chat for Flutter apps: a streaming
chat widget, a floating launcher, and server-driven UI (kletso.ui/v1) rendered
through your own widgets. Pure Dart, no platform channels, works on
Android, iOS, web (JS and Wasm) and desktop. One dependency.
20-line quick start
import 'package:flutter/material.dart';
import 'package:kletso_flutter/kletso_flutter.dart';
Future<void> main() async {
await Kletso.init(KletsoConfig(publishableKey: 'kl_pub_…', agentId: 'agt_…'));
await Kletso.instance.identifyAnonymous(); // or authenticate(token: jwt)
Kletso.instance
..registerComponent('acme.productCard', (ctx, node) => ProductCard(
name: node.string('name'),
price: node.number('price'),
onView: () => ctx.executeAction('view'),
))
..registerAction('open_product', (args, ctx) =>
Navigator.of(ctx.context).pushNamed('/product/${args['sku']}'))
..onOpenUrl = (uri) => launchUrl(uri); // url_launcher, or your router
runApp(const MyApp());
}
// anywhere in the tree:
// KletsoLauncher() floating bubble → opens the chat sheet
// Kletso.instance.open(context) open programmatically
// KletsoChat() embed the chat in your own page
Try it without a backend:
final backend = KletsoFakeBackend(scenario: const KletsoFakeScenario.demo());
await Kletso.init(config, api: backend, transport: backend);
What you get
| widget | purpose |
|---|---|
KletsoChat |
header, connection banner, reverse message list, streaming bubbles, tool-call chips, rendered surfaces, typing indicator, composer |
KletsoLauncher |
floating bubble with unread dot; open() shows a sheet (phone) or a docked panel (wide screens) or a full-screen route |
KletsoSurfaceView |
renders one kletso.ui/v1 surface; use it to preview surfaces or embed them outside the chat |
KletsoConversationList |
the user's conversations with a "new conversation" row |
KletsoTheme |
ThemeExtension generated from the Kletso design tokens; KletsoTheme.fromServer(bootstrap.theme) applies your dashboard branding |
Custom components
The agent decides what to show; your app decides how. Register a builder
per type; the SDK resolves { "path": … } bindings, enforces limits (≤ 500
components, depth ≤ 16, ≤ 256 KB) and falls back to plain text for anything
unknown, so a model mistake never crashes your app.
Kletso.instance.registerComponent(productCardSpec.type, (ctx, node) => ProductCard(
sku: node.string('sku'), // typed getters never throw
price: node.number('price'),
inStock: node.boolean('inStock'),
onView: () => ctx.executeAction('view'), // local → your registerAction handler
onAdd: () => ctx.executeAction('add'), // agent → posted back as the user's next turn
), spec: productCardSpec);
Declare the props schema once as a KletsoComponentSpec in
lib/kletso_components.dart (import package:kletso_core only, no Flutter)
and run dart run kletso_flutter:sync to produce kletso.components.json for
the dashboard, so the model's render_ui tool schema always matches your
widgets.
Built-ins shipped (30): text, markdown, card, row, column, divider, image,
avatar, button, list, table, chart (bar/line/pie, no dependency), carousel,
form, input, select, confirm, loading, error, badge, link, map (schematic
map with markers and route), video and audio (poster/waveform, open
through onOpenUrl), rating (interactive stars), steps (timeline),
accordion, tabs, countdown (live), progress. Host builders may
override any of them by name: the example app swaps map for flutter_map
tiles and video/audio for video_player, so the agent emits the same JSON
and the app decides how rich it looks.
Avatar pictures per mood
With the avatar style set to Your image in the dashboard, the host app can
show its own pictures instead of the mascot, one per mood. KletsoChat and
KletsoLauncher pick them up from the session bootstrap automatically; a
hand-rolled surface does the same through KletsoClientAvatar:
final avatar = KletsoClientAvatar(client);
// avatar.imageFor(KletsoAvatarMood.happy) → the URL from the dashboard, or null
Widget face = avatar.build(size: 96); // crossfades between moods, breathes, bounces while speaking
Or drive KletsoAvatar directly with moodImages (any ImageProvider,
including animated GIF/WebP). Pictures change at runtime whenever a new
bootstrap arrives; nothing to restart.
Loaders
While the model is still writing a surface the runtime streams ui.pending
(component ids and types seen so far); the chat shows a placeholder per
component and swaps in the real one on ui.render. Give each type the right
shape:
Kletso.instance.registerLoader('product_card', (ctx) => const ProductCardSkeleton());
Types without a loader get the SDK's shimmer. A loading block with
for/count uses the same loaders from JSON.
Every interaction direction
| direction | how |
|---|---|
| app → Kletso | track, screen, setContext/updateContext, open, close, useAgent, logout, pause/resume |
| Kletso → app UI | streamed text, ui.render surfaces, proactive trigger turns |
| user in chat → agent | agent/submit/confirm/workflow actions and text |
| user in chat → your app | local actions → registerAction handler (navigate, add to cart, ctx.client!.close()); echoed to the runtime as analytics only |
| both in one tap | your builder changes app state and calls ctx.executeAction('add'); or a local handler calls ctx.client!.send(...) |
| agent → your app, no tap | app.command → your registered handler, only with KletsoConfig(allowServerCommands: true) and client.ui.contextProvider set; outcomes on client.ui.commandResults |
| silent app event → proactive UI | track('geofence_entered') → server rule → app.notify → KletsoNotificationHost banner/toast/alert (or your OS tray via onSystemNotification); tap opens the chat and/or runs a local/url/agent action |
| runtime → device while backgrounded | push: registerPushToken(KletsoPushToken(...)) once, then handlePushPayload(message.data) from your push plugin; same rendering and dedupe as live |
Triggers: track('cart_abandoned') and screen('checkout') let the agent
speak first. A trigger that opens a new conversation still reaches the device
(session-level events), lights the launcher's unread dot, and open() lands
on that conversation.
Proactive notifications
MaterialApp(
navigatorKey: navigatorKey,
builder: (context, child) =>
KletsoNotificationHost(client: Kletso.instance, child: child!),
);
Kletso.instance.ui.contextProvider = () => navigatorKey.currentContext;
// optional: OS tray for `system` notifications (your plugin, not ours)
Kletso.instance.onSystemNotification = (n) async {
await localNotifications.show(/* n.title, n.payload.body, payload: n.id */);
return true; // false → the SDK shows its banner instead
};
// on OS-notification tap: Kletso.instance.ui.openNotification(n)
// push while backgrounded:
await Kletso.instance.registerPushToken(
KletsoPushToken(platform: KletsoPushPlatform.fcm, token: fcmToken),
);
FirebaseMessaging.onMessage.listen((m) => Kletso.instance.handlePushPayload(m.data));
Channels: banner (top card, auto-dismiss), toast (bottom pill), alert
(dialog), system (your tray, banner fallback), silent (no UI: just open the
chat / run the action). Payloads may carry an inline kletso.ui/v1 surface and a
tap action. Outcomes stream on client.ui.notificationResults.
Security defaults
- Only the publishable key ships in the app; user identity is a host-signed token.
- Markdown renders links only; no inline HTML. Links, images and
urlactions pass the session's host allowlist;javascript:/data:/http:never open. - Payload size, depth and count limits are enforced before anything is built.
Markdown
KletsoPlainMarkdownRenderer (default) covers headings, emphasis, code, lists,
quotes, rules and links with zero dependencies and is Wasm-clean. Pass another
KletsoMarkdownRenderer (e.g. wrapping gpt_markdown) to KletsoChat if you
need tables or LaTeX.
Dependencies
flutter, kletso_core, kletso_ui_schema, meta. Fonts: Plus Jakarta Sans
(OFL) bundled; KletsoTheme.light(useHostFont: true) to inherit yours.
Libraries
- kletso_flutter
- Embeddable Kletso AI chat for Flutter.