chat_pilot_kit_flutter 1.0.0-beta.9 copy "chat_pilot_kit_flutter: ^1.0.0-beta.9" to clipboard
chat_pilot_kit_flutter: ^1.0.0-beta.9 copied to clipboard

Flutter widgets and Riverpod integration for chat_pilot_kit_dart.

chat_pilot_kit_flutter #

pub package License: MIT

English | 简体中文

Flutter widgets and Riverpod integration for chat_pilot_kit_dart.

Provides ready-made conversation UI, 9 built-in node views, theming, streaming markdown rendering, and a NodeView extension system.

Features #

  • 🎯 Ready-to-use NodeViews — 9 built-in node renderers (text, markdown, thinking, tool_call, image, file, audio, video, files_group)
  • 🧩 NodeView Extension System — Three extension patterns: customViewBuilders, flutterNodeView, getDefaultFlutterExtensions
  • 🎨 Theme SystemChatPilotKitTheme + ChatPilotKitThemeScope with automatic light/dark mode support
  • 🌊 Streaming Rendering — Real-time markdown and thinking block updates with no extra setup
  • 🔌 Riverpod Integration — Out-of-the-box providers for reactive state management
  • 📘 Full Dart Type Support — Strongly typed API aligned with React SDK

Install #

dependencies:
  chat_pilot_kit_flutter: ^1.0.0-beta.1

chat_pilot_kit_dart is re-exported automatically — no need to add it separately.

Quick Start #

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

void main() {
  // Store once — reuse for both controller and ConversationListView
  final extensions = getDefaultFlutterExtensions();
  
  final controller = createChatPilotKit(
    agentService: MyAgentService(),
    overrideExtensions: extensions,
  );

  runApp(
    ChatPilotKitScope(
      controller: controller,
      child: const MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ConversationListView(
          extensions: extensions,
          padding: const EdgeInsets.all(12),
        ),
      ),
    );
  }
}

ChatPilotKitScope provides the controller to all descendants via Riverpod. ConversationListView renders all conversations with built-in node views.

Core Concepts #

AgentService — Data Injection #

All node data is injected through the onData() method of AgentService. Extend BaseAgentService and implement your backend adapter:

class MyAgentService extends BaseAgentService {
  @override
  Future<void> query(
    String text, {
    List<AttachmentInput>? attachments,
    QueryOptions? options,
  }) async {
    // Call your AI backend (SSE/WebSocket/REST, etc.)
    await for (final chunk in connectToBackend(text)) {
      // Inject data via onData, SDK routes to Extension by nodeType
      onData(AgentMessageData(
        answer: chunk.text,           // Text content (incremental chunk)
        nodeType: chunk.type,         // 'markdown', 'thinking', 'image', ...
        nodeData: chunk.data,         // Node-specific structured data (see below)
        queryId: queryId,             // Current query ID
        sessionId: sessionId,         // Session ID
        nodeCompleted: chunk.isLast,  // Whether this is the last chunk for the node
      ));
    }
    
    onCompleted(queryId: queryId, sessionId: sessionId);
  }

  @override
  void abort([String? reason]) {
    // Cancel request
  }
}

AgentMessageData Fields #

Field Type Description
answer String Text content (incremental or complete)
nodeType String? Node type identifier (matches Extension name)
nodeData Map<String, dynamic>? Node-specific structured data
nodeBehavior NodeBehavior? Node behavior (create/append/replace/remove)
nodeCompleted bool Whether this node finished streaming (default false)
targetNode AgentTargetNode? Target node locator (by ID or matcher)
queryId String Query identifier
sessionId String Session identifier

Built-in Node Types & Data Formats #

1. text — Plain Text #

onData(AgentMessageData(
  answer: 'This is plain text',
  nodeType: 'text',
  queryId: queryId,
  sessionId: sessionId,
));

2. markdown — Markdown Rendering (Streamable) #

onData(AgentMessageData(
  answer: '# Heading\n\nThis is **bold** text',
  nodeType: 'markdown',
  queryId: queryId,
  sessionId: sessionId,
  nodeCompleted: false,  // Streaming chunk
));

3. image — Image #

onData(AgentMessageData(
  answer: '',
  nodeType: 'image',
  nodeData: {
    'url': 'https://example.com/image.jpg',
    'alt': 'Image description',
    'caption': 'Image caption (optional)',
  },
  queryId: queryId,
  sessionId: sessionId,
));

4. file — File #

onData(AgentMessageData(
  answer: '',
  nodeType: 'file',
  nodeData: {
    'url': 'https://example.com/document.pdf',
    'fileName': 'report.pdf',
    'fileSize': 2048000,  // bytes
    'fileType': 'application/pdf',
  },
  queryId: queryId,
  sessionId: sessionId,
));

5. audio — Audio #

onData(AgentMessageData(
  answer: '',
  nodeType: 'audio',
  nodeData: {
    'url': 'https://example.com/audio.mp3',
    'fileName': 'speech.mp3',
    'duration': 120.5,  // seconds (optional)
  },
  queryId: queryId,
  sessionId: sessionId,
));

6. video — Video #

onData(AgentMessageData(
  answer: '',
  nodeType: 'video',
  nodeData: {
    'url': 'https://example.com/video.mp4',
    'poster': 'https://example.com/thumb.jpg',  // thumbnail (optional)
    'duration': 180.0,  // seconds (optional)
  },
  queryId: queryId,
  sessionId: sessionId,
));

7. thinking — Thinking Block (Streamable) #

onData(AgentMessageData(
  answer: 'Analyzing data...',
  nodeType: 'thinking',
  nodeData: {
    'title': 'Thinking',  // optional
    'status': 'processing',  // optional
  },
  queryId: queryId,
  sessionId: sessionId,
  nodeCompleted: false,
));

8. tool_call — Tool Call #

onData(AgentMessageData(
  answer: '',
  nodeType: 'tool_call',
  nodeData: {
    'name': 'search_web',
    'input': {'query': 'Flutter tutorial'},
    'output': {'results': [...]},  // optional
    'status': 'completed',  // pending/running/completed/error
  },
  queryId: queryId,
  sessionId: sessionId,
));

9. files_group — Files Group (Streamable Append) #

For displaying multiple files (images, videos, documents) at once with automatic grouped layout:

onData(AgentMessageData(
  answer: '',
  nodeType: 'files_group',
  nodeData: {
    'items': [
      {
        'url': 'https://example.com/img1.jpg',
        'fileName': 'photo1.jpg',
        'fileType': 'image/jpeg',
      },
      {
        'url': 'https://example.com/video.mp4',
        'fileName': 'demo.mp4',
        'fileType': 'video/mp4',
        'poster': 'https://example.com/thumb.jpg',
        'duration': 60.0,
      },
      {
        'url': 'https://example.com/doc.pdf',
        'fileName': 'report.pdf',
        'fileType': 'application/pdf',
        'fileSize': 1024000,
      },
    ],
  },
  queryId: queryId,
  sessionId: sessionId,
));

Streaming Append Example (RAG scenario):

// First batch
onData(AgentMessageData(
  nodeType: 'files_group',
  nodeData: {'items': [item1, item2]},
  nodeBehavior: NodeBehavior.create,
  queryId: queryId,
  sessionId: sessionId,
));

// Append more
onData(AgentMessageData(
  nodeType: 'files_group',
  nodeData: {'items': [item3, item4]},
  nodeBehavior: NodeBehavior.append,  // Append to existing node
  queryId: queryId,
  sessionId: sessionId,
));

Getting Controller Instance #

There are three ways to get the ChatPilotKitController instance to call controller.query() and other methods:

Use ref.watch() or ref.read() anywhere inside ChatPilotKitScope:

import 'package:flutter_riverpod/flutter_riverpod.dart';

// ConsumerWidget approach
class ChatPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Get controller instance
    final controller = ref.watch(chatPilotKitControllerProvider);
    final isQuerying = ref.watch(isQueryingProvider).value ?? false;

    return Column(
      children: [
        Expanded(child: ConversationListView(extensions: extensions)),
        ElevatedButton(
          onPressed: isQuerying
              ? controller.interrupt
              : () => controller.query('Hello'),  // ✅ Direct call
          child: Text(isQuerying ? 'Stop' : 'Send'),
        ),
      ],
    );
  }
}

// Consumer approach
class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        final controller = ref.watch(chatPilotKitControllerProvider);
        
        return ElevatedButton(
          onPressed: () => controller.query('Hello'),  // ✅ Direct call
          child: Text('Send'),
        );
      },
    );
  }
}

Method 2: Use ref.read() in Callbacks (No Rebuild) #

If you only need it in callbacks and don't want to listen for changes, use ref.read():

class ChatInput extends ConsumerWidget {
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return TextField(
      controller: textController,
      onSubmitted: (text) {
        // ✅ Use ref.read() in callbacks, won't cause rebuild
        final controller = ref.read(chatPilotKitControllerProvider);
        controller.query(text);
        textController.clear();
      },
    );
  }
}

Method 3: Save Controller Reference (Use Outside Scope) #

If you need to use it outside ChatPilotKitScope, save a reference when creating:

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final ChatPilotKitController controller;
  late final List<MessageExtensionInstance> extensions;

  @override
  void initState() {
    super.initState();
    extensions = getDefaultFlutterExtensions();
    
    // ✅ Create and save controller reference
    controller = createChatPilotKit(
      agentService: MyAgentService(),
      overrideExtensions: extensions,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return ChatPilotKitScope(
      controller: controller,
      child: MaterialApp(
        home: Scaffold(
          body: ConversationListView(extensions: extensions),
          floatingActionButton: FloatingActionButton(
            // ✅ Use saved controller reference directly
            onPressed: () => controller.query('Hello'),
            child: Icon(Icons.send),
          ),
        ),
      ),
    );
  }
}

Providers #

All providers are automatically available inside ChatPilotKitScope:

Provider Type Description
chatPilotKitControllerProvider Provider<ChatPilotKitController> The main controller instance
conversationsProvider StreamProvider<List<ConversationBean>> Reactive conversation list
isQueryingProvider StreamProvider<bool> Whether a query is in progress
nodeInteractionProvider StreamProvider<NodeInteractionEvent> Global node interaction stream
filteredNodeInteractionProvider StreamProvider.family Filtered node interaction stream

Controller Common Methods #

final controller = ref.watch(chatPilotKitControllerProvider);

// Send query
await controller.query('Hello');

// Query with attachments
await controller.queryWithAttachments(
  'Analyze this file',
  [AttachmentInput(url: 'file:///path/to/file.pdf', ...)],
);

// Interrupt current query
controller.interrupt();

// Clear all conversations
controller.clear();

// Export conversations
final snapshots = controller.exportConversations();

// Import conversations
controller.importConversations(snapshots);

// Get current conversations
final conversations = controller.conversations;

Event Listening #

controller.events is a Stream<ChatEvent>. All events are emitted through this stream. Use Dart 3 pattern matching or type checking to listen for specific events:

// Method 1: Using if type checking
controller.events.listen((event) {
  if (event is InterruptEvent) {
    print('Query interrupted: queryId=${event.queryId}');
  }
  else if (event is ErrorEvent) {
    print('Error occurred: ${event.error}');
  }
  else if (event is TtftEvent) {
    print('TTFT: ${event.totalLatency}ms');
  }
});

// Method 2: Using Dart 3 switch pattern matching
controller.events.listen((event) {
  switch (event) {
    case InterruptEvent(:final queryId, :final sessionId):
      print('Query interrupted: $queryId');
    case ErrorEvent(:final error, :final queryId):
      print('Error: $error, queryId: $queryId');
    case TtftEvent(:final totalLatency):
      print('TTFT: ${totalLatency}ms');
    case ConversationAddEvent(:final conversationId):
      print('New conversation: $conversationId');
    case NodeAddEvent(:final nodeType, :final nodeId):
      print('New node: $nodeType, id: $nodeId');
    case ReadyEvent():
      print('System ready');
    // ... other event types
  }
});

// Method 3: Using StreamBuilder in a Widget
StreamBuilder<ChatEvent>(
  stream: controller.events.where((e) => e is InterruptEvent),
  builder: (context, snapshot) {
    if (snapshot.hasData && snapshot.data is InterruptEvent) {
      final event = snapshot.data as InterruptEvent;
      return Text('Query ${event.queryId} interrupted');
    }
    return const SizedBox.shrink();
  },
)

Available Event Types

Event Type Description Key Fields
ConversationAddEvent New conversation added conversationId, bean
ConversationChangeEvent Conversation updated conversationId, nodes, completed
NodeAddEvent New node added nodeId, nodeType, node
NodeUpdateEvent Node updated nodeId, nodeType, node
InterruptEvent Query interrupted queryId, sessionId
ErrorEvent Error occurred error, queryId, sessionId
TtftEvent Time-to-first-token metric totalLatency, queryId
ClearEvent Conversations cleared -
ReadyEvent System ready -
HistoryImportEvent History import completed count, position

Subscription Management

Remember to cancel subscriptions when no longer needed:

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  StreamSubscription<ChatEvent>? _eventSubscription;

  @override
  void initState() {
    super.initState();
    final controller = ref.read(chatPilotKitControllerProvider);
    _eventSubscription = controller.events.listen((event) {
      if (event is InterruptEvent) {
        // Handle interrupt event
      }
    });
  }

  @override
  void dispose() {
    _eventSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Built-in Node Views #

Node Type Widget Streamable nodeData Fields
text TextNodeView No -
markdown MarkdownNodeView Yes -
image ImageNodeView No url, alt, caption
audio AudioNodeView No url, fileName, duration
video VideoNodeView No url, poster, duration
file FileNodeView No url, fileName, fileSize, fileType
thinking ThinkingBlockNodeView Yes title, status
tool_call ToolCallNodeView No name, input, output, status
files_group FilesGroupView Yes (append) items[] (each with url, fileName, fileType, etc.)

NodeView Extension System #

1. customViewBuilders — Register a new node type #

ConversationListView(
  extensions: extensions,
  customViewBuilders: {
    'rating': (props) => RatingNodeView(props: props),
    'chart': (props) => ChartWidget(
      data: (props.node as GenericNode).content,
    ),
  },
)

Note: Custom node types also require registering a corresponding Extension to process the data, otherwise nodes won't be created.

class RatingExtension extends MessageExtensionConfig<GenericNode> {
  @override
  String get name => 'rating';

  @override
  bool canProcess(AgentMessageData data) => data.nodeType == 'rating';

  @override
  GenericNode process(AgentMessageData data, [Map<String, dynamic> options = const {}]) {
    return GenericNode(
      customType: 'rating',
      content: data.nodeData ?? {},
    );
  }
}

// Register to Controller
final controller = createChatPilotKit(
  agentService: myService,
  extensions: [MessageExtensionInstance(RatingExtension())],
);

2. flutterNodeView — Override a built-in NodeView #

Keep the extension's processing/streaming logic, replace only the UI:

final styledText = flutterNodeView(
  TextExtension(),
  (props) => MyStyledTextView(props: props),
);

final controller = createChatPilotKit(
  agentService: service,
  overrideExtensions: [styledText, ...otherExtensions],
);

Equivalent to React SDK's Extension.extend({ addNodeView() { return ReactNodeViewRenderer(Component); } }).

3. getDefaultFlutterExtensions — Pre-bound extension set #

// Store once — reuse for both controller and ConversationListView
final extensions = getDefaultFlutterExtensions();

final controller = createChatPilotKit(
  agentService: service,
  overrideExtensions: extensions,
);

// Pass to ConversationListView for Extension-level NodeView dispatch
ConversationListView(extensions: extensions)

Equivalent to React SDK's getDefaultReactExtensions().

NodeViewProps #

Every node view receives NodeViewProps:

Field Type Description
node ConversationNode The node instance
conversation ConversationBean Parent conversation
role ConversationRole .client or .aiWorker
completed bool Whether the node is done streaming
updateContent Function(Object)? Update node content
updateMetadata Function(Map)? Update node metadata
emitInteraction Function(String, {data})? Emit interaction event
destroy Function()? Dispose the node
onCommand StreamSubscription Function(handler)? Subscribe to host commands (requires manual cancellation)

Getting Node Type #

In custom NodeViews, access the node type via props.node.type and cast to specific node instances to access typed content:

class MyCustomNodeView extends StatelessWidget {
  final NodeViewProps props;

  const MyCustomNodeView({super.key, required this.props});

  @override
  Widget build(BuildContext context) {
    // 1. Get node type (string)
    final nodeType = props.node.type;  // 'markdown', 'image', 'files_group', etc.
    
    // 2. Cast to specific node instance by type
    if (props.node is MarkdownNode) {
      final markdownNode = props.node as MarkdownNode;
      final content = markdownNode.content;  // String
      return Text('Markdown: $content');
    }
    
    if (props.node is ImageNode) {
      final imageNode = props.node as ImageNode;
      final content = imageNode.content;  // ImageContent
      return Image.network(content.url);
    }
    
    if (props.node is FilesGroupNode) {
      final filesGroupNode = props.node as FilesGroupNode;
      final items = filesGroupNode.content.items;  // List<FilesGroupItem>
      return Text('Files group contains ${items.length} files');
    }
    
    // 3. Generic node (custom types)
    if (props.node is GenericNode) {
      final genericNode = props.node as GenericNode;
      final customType = genericNode.customType;  // 'rating', 'chart', etc.
      final content = genericNode.content;  // Map<String, dynamic>
      return Text('Custom node: $customType');
    }
    
    return const Text('Unknown node type');
  }
}

Built-in Node Type Mapping #

Node Type String Node Class Content Type
'text' TextNode String
'markdown' MarkdownNode String
'image' ImageNode ImageContent
'audio' AudioNode AudioContent
'video' VideoNode VideoContent
'file' FileNode FileContent
'thinking' ThinkingNode ThinkingContent
'tool_call' ToolCallNode ToolCallContent
'files_group' FilesGroupNode FilesGroupContent
Custom GenericNode Map<String, dynamic>

Streaming Content Updates #

For streamable nodes, use StreamBuilder to listen for content changes:

class MyMarkdownView extends StatelessWidget {
  final NodeViewProps props;

  const MyMarkdownView({super.key, required this.props});

  @override
  Widget build(BuildContext context) {
    final node = props.node as MarkdownNode;
    
    return StreamBuilder<String>(
      stream: node.onContentUpdate,  // Stream<String>
      initialData: node.content,
      builder: (context, snapshot) {
        final content = snapshot.data ?? '';
        final isStreaming = !props.completed;
        
        return Column(
          children: [
            Text(content),
            if (isStreaming) const CircularProgressIndicator(),
          ],
        );
      },
    );
  }
}

Node Interaction #

// Listen to interactions from NodeViews
ref.listen(nodeInteractionProvider, (prev, next) {
  final event = next.value;
  if (event != null) {
    print('[${event.type}] nodeId=${event.nodeId} data=${event.data}');
  }
});

// Filter by node type
final toolCallSub = ref.watch(
  filteredNodeInteractionProvider(
    (event) => event.nodeType == 'tool_call',
  ),
);

// Send a command to a specific node
sendNodeCommand(controller, nodeId: 'node-123', type: 'highlight');

Theming #

Node views use ChatPilotKitTheme for consistent styling. By default, colors are derived from the surrounding ThemeData.

ChatPilotKitThemeScope(
  theme: ChatPilotKitTheme(
    bubbleAiBackground: Colors.grey.shade100,
    bubbleUserBackground: Colors.blue,
    bubbleAiForeground: Colors.black87,
    bubbleUserForeground: Colors.white,
    accent: Colors.blue,
    codeBackground: const Color(0xFFF6F8FA),
    codeForeground: const Color(0xFF1F2328),
    inlineCodeBackground: const Color(0x1F818B98),
    borderColor: const Color(0xFFD1D9E0),
    mutedForeground: const Color(0xFF59636E),
    bubbleRadius: 16,
  ),
  child: ConversationListView(extensions: extensions),
)

Access in custom widgets:

final cpkTheme = ChatPilotKitThemeScope.of(context);

Streaming #

Streamable nodes (markdown, thinking, files_group) automatically update in real-time via StreamBuilder on node.onContentUpdate. No extra setup needed.

Attachments #

await controller.queryWithAttachments(
  'Check this file',
  [
    AttachmentInput(
      url: 'file:///path/to/doc.pdf',
      fileName: 'doc.pdf',
      fileSize: 2048000,
      fileType: 'application/pdf',
    ),
  ],
);

Attachments are automatically converted to corresponding nodes (ImageNode, FileNode, etc.) based on MIME type and displayed in the user message bubble.

Conversation Persistence #

// Export
final snapshots = controller.exportConversations();
final json = snapshots.map((s) => s.toJson()).toList();

// Import (prepend, default)
controller.importConversations(inputs);

// Import (replace all)
controller.importConversations(
  inputs,
  options: const ImportOptions(position: ImportPosition.replace),
);

API Exports #

chat_pilot_kit_flutter exports all available APIs:

1. Complete Re-export of chat_pilot_kit_dart #

import 'package:chat_pilot_kit_flutter/chat_pilot_kit_flutter.dart';

// All chat_pilot_kit_dart content is directly available, including:
// - createChatPilotKit()
// - BaseAgentService
// - AgentMessageData
// - ConversationNode (and all node types)
// - MessageExtensionConfig
// - ChatPilotKitController
// - All event types (ConversationAddEvent, NodeAddEvent, etc.)

2. Theme System #

// Theme configuration
import 'package:chat_pilot_kit_flutter/chat_pilot_kit_flutter.dart';

ChatPilotKitTheme       // Theme data class
ChatPilotKitThemeScope  // Theme scope widget

3. Riverpod Providers #

// Providers (use inside ChatPilotKitScope)
chatPilotKitControllerProvider       // Provider<ChatPilotKitController>
conversationsProvider                // StreamProvider<List<ConversationBean>>
isQueryingProvider                   // StreamProvider<bool>
nodeInteractionProvider              // StreamProvider<NodeInteractionEvent>
filteredNodeInteractionProvider      // StreamProvider.family<NodeInteractionEvent>

4. Core Widgets #

// Container widget
ChatPilotKitScope         // Controller scope provider

// Conversation list
ConversationListView      // Full conversation list renderer

// Node renderer
NodeRenderer              // Single node renderer (supports Extension dispatch)
NodeViewProps             // Node view properties class
NodeViewBuilder           // Node view builder type

5. Extension System #

// Flutter extension utilities
getDefaultFlutterExtensions()  // Get pre-bound default extensions
flutterNodeView()              // Create Flutter NodeView extension

6. Built-in NodeView Components (9 types) #

// Text types
TextNodeView              // Plain text node view
MarkdownNodeView          // Markdown rendering view

// Media types
ImageNodeView             // Image node view
AudioNodeView             // Audio node view
VideoNodeView             // Video node view
FileNodeView              // File node view
FilesGroupView            // Files group node view (multiple files)

// AI special types
ThinkingBlockNodeView     // Thinking block node view
ToolCallNodeView          // Tool call node view

7. Helper Components #

ThinkingIndicator         // Thinking animation indicator
StreamingDots             // Streaming dots indicator (animated bouncing dots)

8. Node Interaction Utilities #

// Node interaction functions
sendNodeCommand()         // Send command to node

Complete Import Example #

import 'package:chat_pilot_kit_flutter/chat_pilot_kit_flutter.dart';

// ✅ All content imported at once, including:
// - All chat_pilot_kit_dart APIs
// - Flutter UI components
// - Riverpod Providers
// - Theme system
// - Extension utilities

Selective Import Example #

If you only need specific content, you can import precisely:

// Only core functionality
import 'package:chat_pilot_kit_dart/chat_pilot_kit_dart.dart';

// Only Flutter layer
import 'package:chat_pilot_kit_flutter/src/widgets/conversation_list_view.dart';
import 'package:chat_pilot_kit_flutter/src/providers/chat_pilot_kit_scope.dart';
import 'package:chat_pilot_kit_flutter/src/theme/cpk_theme.dart';

Recommended: Use import 'package:chat_pilot_kit_flutter/chat_pilot_kit_flutter.dart'; to import everything at once for simplicity.

Full Example #

See the example app for a complete playground with:

  • Chat tab with text input and attachment support
  • Demos tab with behavior / target / media / NodeView extension demos
  • Settings tab with theme switching and NodeView gallery
  • NodeView Extension page showing all 3 extension patterns
cd example && flutter run -d <device>
Package Description
chat_pilot_kit_dart Headless core SDK, pure Dart with no Flutter dependency

Publishing #

yarn dart:publish:dry   # dry run
yarn dart:publish       # publish to pub.dev

License #

MIT