ib_conversations 0.0.13-alpha
ib_conversations: ^0.0.13-alpha copied to clipboard
AI Conversations Flutter widget library
example/lib/main.dart
// example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:file_picker/file_picker.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'package:path/path.dart' as path;
import 'dart:convert';
import 'package:intl/intl.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:ib_conversations/ib_conversations.dart' as ib_conversations;
final ib_conversations.EmbeddedWidgetRegistry globalWidgetRegistry =
ib_conversations.EmbeddedWidgetRegistry();
void main() async {
// Ensure Flutter widgets binding is initialized before loading dotenv
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
ib_conversations.registerAllEmbeddedWidgets(globalWidgetRegistry);
print('--- Registered Widget Metadata (from example main.dart) ---');
globalWidgetRegistry.getAllMetadata().forEach((label, metadata) {
print('Label: $label');
print(' Description: ${metadata.description}');
print(' Parameters: ${metadata.parameterSchema}');
});
print('----------------------------------------------------------');
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<ib_conversations.EmbeddedWidgetRegistry>.value(
value: globalWidgetRegistry,
child: MaterialApp(
title: 'ib_conversations Example App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ChatPage(userId: 'example_user_123'),
),
);
}
}
/// A simple example page that hosts the ConversationWidget.
class ChatPage extends StatefulWidget {
final String userId;
const ChatPage({Key? key, required this.userId}) : super(key: key);
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final String _backendBaseUrl =
dotenv.env['API_URL'] ?? 'http://localhost:8080';
final String _websocketBaseUrl =
dotenv.env['WEBSOCKET_URL'] ?? 'ws://localhost:8080';
int _responseCycleCounter = 0;
static const String _response1_dataDisplay_actionButton = """
Hello! Here's some information:
```widget:data_display
{
"text": "This is dynamic data from the 'backend'."
}
```
And here's an action you can take:
```widget:action_button
{
"label": "Click Me for Action!",
"commandType": "example_action",
"args": {
"source": "button_1",
"value": 123
}
}
```
That's all for now.
""";
static const String _response2_questionnaire_single = """
Please answer the following question:
```widget:questionnaire
{
"questionId": "q1_mood",
"questionText": "How are you feeling today?",
"selectionType": "single",
"commandType": "submit_mood_survey",
"options": [
{"value": "happy", "text": "Happy 😊"},
{"value": "neutral", "text": "Neutral 😐"},
{"value": "sad", "text": "Sad 😢"}
],
"args": {"survey_version": "1.0"}
}
```
Your response will be recorded.
""";
static const String _response3_questionnaire_multiple_actionButton = """
Select all symptoms you are experiencing:
```widget:questionnaire
{
"questionId": "q2_symptoms",
"questionText": "Which symptoms are you experiencing? (Select all that apply)",
"selectionType": "multiple",
"commandType": "submit_symptoms",
"options": [
{"value": "headache", "text": "Headache"},
{"value": "fever", "text": "Fever"},
{"value": "cough", "text": "Cough"},
{"value": "fatigue", "text": "Fatigue"}
],
"args": {"patient_id": "P789"}
}
```
Once you've made your selections, you can also trigger another action:
```widget:action_button
{
"label": "Request Nurse Call",
"commandType": "request_nurse_call",
"args": {"urgency": "medium"}
}
```
""";
static const String _response4_monitoringTask = """
It's time to check your blood pressure. Please use the device and take a photo of the reading.
```widget:monitoring_task
{
"taskId": "bp_check_001",
"taskDescription": "Blood Pressure Monitoring: Position the cuff correctly, take a reading, and then take a clear photo of the device's display showing both systolic and diastolic numbers.",
"monitoringType": "blood_pressure",
"commandType": "submit_bp_reading",
"args": {"device_id": "omron_xyz"}
}
```
""";
static const String _response5_logMealTask = """
Let's log your recent meal.
```widget:log_meal_task
{
"taskId": "meal_log_002",
"taskDescription": "Log Your Lunch: Please provide details about your lunch. You can take a photo, describe it, or search for a recipe.",
"commandType": "submit_meal_log_details",
"initialCuisine": "mediterranean",
"args": {"meal_type": "lunch"}
}
```
""";
static const String _response6_mixed = """
Here's a summary and a few tasks for you:
First, some data we have on file:
```widget:data_display
{
"text": "Patient ID: P-\${DateTime.now().millisecondsSinceEpoch}"
}
```
Next, a quick question about your sleep:
```widget:questionnaire
{
"questionId": "q3_sleep",
"questionText": "How was your sleep last night?",
"selectionType": "single",
"commandType": "submit_sleep_quality",
"options": [
{"value": "good", "text": "Good"},
{"value": "fair", "text": "Fair"},
{"value": "poor", "text": "Poor"}
]
}
```
And finally, an important task:
```widget:monitoring_task
{
"taskId": "glucose_check_003",
"taskDescription": "Blood Glucose Check: Please take a photo of your glucose meter reading.",
"monitoringType": "blood_sugar",
"commandType": "submit_glucose_reading"
}
```
Let me know if you have any questions! You can also click this:
```widget:action_button
{
"label": "Mark All as Done (Simulated)",
"commandType": "mark_tasks_done",
"args": {"tasks": ["q3_sleep", "glucose_check_003"]}
}
```
""";
final List<String> _mockResponses = [
_response1_dataDisplay_actionButton,
_response2_questionnaire_single,
_response3_questionnaire_multiple_actionButton,
_response4_monitoringTask,
_response5_logMealTask,
_response6_mixed,
];
Future<String?> _sendChatDataToBackend({
required String message,
String? flavor,
List<PlatformFile>? files,
Map<String, dynamic>? additionalArgs,
}) async {
print('--- Example App: Sending chat data to backend ---');
print(' Message: "$message"');
print(' Flavor: "$flavor"');
print(' Files attached: ${files?.length ?? 0}');
if (files != null) {
for (var file in files) {
print(' - File: ${file.name} ({file.size} bytes)');
}
}
print(' Additional Args: $additionalArgs');
print(
' Target URL: $_backendBaseUrl/api/v1/ai/chat',
);
final uri = Uri.parse('$_backendBaseUrl/api/v1/ai/chat');
final request = http.MultipartRequest('POST', uri);
request.fields['text'] = message;
if (flavor != null) {
request.fields['flavor'] = flavor;
}
if (files != null && files.isNotEmpty) {
for (var file in files) {
if (file.bytes != null) {
request.files.add(
http.MultipartFile.fromBytes(
'files',
file.bytes!,
filename: file.name,
),
);
print(' - Added file from bytes: ${file.name}');
} else if (file.path != null) {
request.files.add(
await http.MultipartFile.fromPath(
'files',
file.path!,
filename: path.basename(
file.path!,
),
),
);
print(' - Added file from path: ${file.name}');
} else {
print(
' - Warning: Attached file ${file.name} has neither bytes nor path. Skipping.',
);
}
}
}
if (additionalArgs != null) {
additionalArgs.forEach((key, value) {
request.fields[key] = value.toString();
});
}
await Future.delayed(
Duration(milliseconds: 500 + _responseCycleCounter * 100),
);
final simulatedResponse =
_mockResponses[_responseCycleCounter % _mockResponses.length];
_responseCycleCounter++;
print('Example App: Returning mock response #${_responseCycleCounter}.');
print('Example App: Mock Markdown string to render: "$simulatedResponse"');
print('--- End Example App: Sending chat data to backend (Mocked) ---');
return simulatedResponse;
}
void _handleEmbeddedWidgetCommand(
String commandType,
Map<String, dynamic> args,
) {
print('--- Example App: Received command from embedded widget ---');
print(' Command Type: "$commandType"');
print(' Arguments: $args');
print('--- End Example App: Received command from embedded widget ---');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Command received: $commandType with args $args'),
duration: Duration(seconds: 3),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Provider<ib_conversations.EmbeddedWidgetRegistry>.value(
value: globalWidgetRegistry,
child: Scaffold(
appBar: AppBar(title: Text('AI Chat Example')),
body: Column(
children: [
Expanded(
child: ib_conversations.ConversationWidget(
width: double.infinity,
height: double.infinity,
user: widget.userId,
showTextInput: true,
flavor: 'DEFAULT',
websocketBaseUrl: _websocketBaseUrl,
primaryColor: theme.primaryColor,
primaryText: theme.textTheme.bodyMedium?.color,
secondaryText: theme.textTheme.bodySmall?.color,
alternateColor: theme.dividerColor,
primaryBackground: theme.scaffoldBackgroundColor,
secondaryBackground: theme.cardColor,
errorColor: theme.colorScheme.error,
bodyMediumStyle: theme.textTheme.bodyMedium,
labelMediumStyle: theme.inputDecorationTheme.hintStyle,
bodySmallStyle: theme.textTheme.bodySmall,
sendChatDataToBackend: _sendChatDataToBackend,
handleEmbeddedWidgetCommand: _handleEmbeddedWidgetCommand,
),
),
],
),
),
);
}
}
extension DateTimeFormatting on DateTime {
String toShortString() {
return DateFormat('yyyy-MM-dd HH:mm').format(this);
}
}