ib_conversations 0.0.10-alpha
ib_conversations: ^0.0.10-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'; // Required for Provider
import 'package:file_picker/file_picker.dart'; // Required for PlatformFile
import 'package:http/http.dart' as http; // Required for making HTTP calls
import 'dart:io'; // Required for File (used by PlatformFile path)
import 'package:path/path.dart' as path; // Required for path.basename
import 'dart:convert'; // Required for jsonDecode
import 'package:intl/intl.dart';
// Import flutter_dotenv to load environment variables
import 'package:flutter_dotenv/flutter_dotenv.dart';
// Import the entire ib_conversations library using its main export file
import 'package:ib_conversations/ib_conversations.dart' as ib_conversations;
// Instantiate and populate the registry once, globally for this example app.
// In a real application, you might manage the lifecycle of this registry
// based on your state management strategy.
final ib_conversations.EmbeddedWidgetRegistry globalWidgetRegistry =
ib_conversations.EmbeddedWidgetRegistry();
void main() async {
// Made main async to potentially load environment variables
// --- Load environment variables using flutter_dotenv ---
// Load the .env file before running the app.
// Ensure you have a .env file in the root of your project and it's listed in pubspec.yaml assets.
await dotenv.load(fileName: ".env");
// --- End Load environment variables ---
// Register all embedded widget libraries during application startup.
// This makes all defined embedded widgets available to the registry.
ib_conversations.registerAllEmbeddedWidgets(globalWidgetRegistry);
// Optional: Print metadata for debugging or backend reference
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) {
// Provide the EmbeddedWidgetRegistry instance high up in the widget tree
// using Provider. This allows ConversationMessage and its builders
// to access the registry via Provider.of(context).
return Provider<ib_conversations.EmbeddedWidgetRegistry>.value(
value: globalWidgetRegistry, // Provide the instantiated registry
child: MaterialApp(
title: 'ib_conversations Example App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
// The home page is a simple ChatPage that will use the ConversationWidget
home: ChatPage(userId: 'example_user_123'),
),
);
}
}
/// A simple example page that hosts the ConversationWidget.
class ChatPage extends StatefulWidget {
final String userId; // Example: User ID for the chat
const ChatPage({Key? key, required this.userId}) : super(key: key);
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
// --- Get your backend base URL from an environment variable using dotenv ---
// Read the API_URL from the loaded environment variables.
// Provide a fallback URL in case the variable is not found (though it's required by the spec).
final String _backendBaseUrl =
dotenv.env['API_URL'] ?? 'http://localhost:8080';
// --- End Get backend base URL ---
// --- Counter to cycle through different mock responses ---
int _responseCycleCounter = 0;
// --- End Counter ---
// --- Pre-defined Markdown responses with various widgets ---
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,
];
// --- End Pre-defined Markdown responses ---
// --- Implement the SendChatDataToBackend callback ---
Future<String?> _sendChatDataToBackend({
required String message,
String? flavor,
List<PlatformFile>? files, // Receive PlatformFile list
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',
); // Print the URL being used
// Define the API endpoint URL
final uri = Uri.parse('$_backendBaseUrl/api/v1/ai/chat');
// Create a multipart request
final request = http.MultipartRequest('POST', uri);
// Add text and flavor fields
request.fields['text'] = message;
if (flavor != null) {
request.fields['flavor'] = flavor;
}
// Add files as multipart files
if (files != null && files.isNotEmpty) {
for (var file in files) {
if (file.bytes != null) {
// Use http.MultipartFile.fromBytes for files with bytes in memory
request.files.add(
http.MultipartFile.fromBytes(
'files', // The field name expected by your backend API for files
file.bytes!,
filename: file.name, // Use the original file name
),
);
print(' - Added file from bytes: ${file.name}');
} else if (file.path != null) {
// Use http.MultipartFile.fromPath for files saved to disk
request.files.add(
await http.MultipartFile.fromPath(
'files', // The field name expected by your backend API for files
file.path!,
filename: path.basename(
file.path!,
), // Use the base name of the file path
),
);
print(' - Added file from path: ${file.name}');
} else {
print(
' - Warning: Attached file ${file.name} has neither bytes nor path. Skipping.',
);
}
}
}
// Optional: Add additional fields from additionalArgs if your API expects them
if (additionalArgs != null) {
additionalArgs.forEach((key, value) {
// Convert non-string values to string if necessary for multipart fields
request.fields[key] = value.toString();
});
}
// --- MODIFIED: Cycle through mock responses instead of actual HTTP call for this example ---
await Future.delayed(
Duration(milliseconds: 500 + _responseCycleCounter * 100),
); // Simulate network delay
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;
// --- END MODIFIED ---
/* --- ORIGINAL HTTP CALL (Commented out for mock testing) ---
try {
// Send the request
final streamedResponse = await request.send();
// Read the response
final response = await http.Response.fromStream(streamedResponse);
print('Example App: Received HTTP response.');
print(' Status Code: ${response.statusCode}');
print(' Response Body: ${response.body}');
if (response.statusCode == 200) {
print('Example App: Backend API call succeeded (Status 200).');
// --- Modified: Parse JSON and extract the 'response' field ---
try {
final jsonResponse = jsonDecode(response.body);
// Assuming the AI response content is under the key 'response'
final aiResponseContent = jsonResponse['response'];
if (aiResponseContent != null) {
// Ensure the extracted content is a String, or convert it
final String responseString = aiResponseContent.toString();
print('Example App: Successfully extracted "response" field.');
print('Example App: Extracted content: "$responseString"');
return responseString;
} else {
print('Example App: JSON response does not contain a "response" field.');
return 'System: AI returned an unexpected response format (missing "response" field). Response: ${response.body}';
}
} catch (e) {
// Handle JSON parsing errors
print('Example App: Error parsing JSON response: $e');
return 'System: Error parsing AI response. Response: ${response.body}';
}
// --- End Modified ---
} else {
// Handle API error status codes
print('Example App: Backend API call failed. Status: ${response.statusCode}');
print('Example App: Response body: ${response.body}');
// You might parse the error body if your API returns structured errors
return 'System: Error contacting AI (${response.statusCode}). Response: ${response.body}'; // Return an error message including status
}
} catch (e, s) {
// Handle exceptions during the HTTP call (e.g., network issues)
print('Example App: Exception during backend interaction: $e\n$s');
return 'System: An unexpected error occurred during backend interaction: ${e.toString()}';
} finally {
print('--- End Example App: Sending chat data to backend ---');
}
--- END ORIGINAL HTTP CALL --- */
}
// --- Implement the SendCommandCallback for embedded widgets ---
// This function handles commands triggered by embedded widgets within messages.
// This is where you define what happens when an embedded widget is interacted with.
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 ---');
// TODO: Implement logic to interpret commands and trigger actions.
// This is where you define what happens when an embedded widget is interacted with.
// - Trigger a specific backend API call based on commandType and args.
// - Update application state (using Provider, setState, etc.).
// - Trigger navigation.
// - Show a different widget in a side canvas/modal (if you implement that UI).
// Example: Show a SnackBar based on the command
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Command received: $commandType with args $args'),
duration: Duration(seconds: 3),
),
);
// Example: If a command is meant to trigger a backend action based on embedded widget interaction
// _sendEmbeddedWidgetCommandToBackend(commandType, args); // You'd implement this method
// Example: Handle canvas mode commands
// if (commandType == 'show_canvas_widget' && args.containsKey('widgetLabel')) {
// // You would update state to show a widget in a side canvas
// // setState(() {
// // _showCanvas = true;
// // _canvasWidgetLabel = args['widgetLabel'];
// // _canvasWidgetParams = args['params'];
// // });
// }
// Example: If a command is meant to hide the canvas
// if (commandType == 'hide_canvas') {
// // setState(() {
// // _showCanvas = false;
// // _canvasWidgetLabel = null;
// // _canvasWidgetParams = null;
// // });
// }
}
@override
Widget build(BuildContext context) {
// Access theme data from Flutter's Theme
final theme = Theme.of(context);
// Provide the widget registry to the subtree where ConversationWidget will be used.
// The ConversationWidget and its children (ConversationMessage, embedded widgets)
// will access the registry via Provider.of(context).
return Provider<ib_conversations.EmbeddedWidgetRegistry>.value(
value: globalWidgetRegistry, // Provide the instantiated registry
child: Scaffold(
appBar: AppBar(title: Text('AI Chat Example')),
body: Column(
children: [
Expanded(
// Use the ConversationWidget from your ib_conversations library
child: ib_conversations.ConversationWidget(
width: double.infinity,
// Match parent width
height: double.infinity,
// Match parent height
user: widget.userId,
showTextInput: true,
// Or pass from page parameters
flavor: 'DEFAULT',
// Or pass from page parameters
// Pass theme parameters from the current Flutter Theme
primaryColor: theme.primaryColor,
primaryText: theme.textTheme.bodyMedium?.color,
// Use null-safe access
secondaryText: theme.textTheme.bodySmall?.color,
// Use null-safe access
alternateColor: theme.dividerColor,
// Example mapping
primaryBackground: theme.scaffoldBackgroundColor,
// Example mapping
secondaryBackground: theme.cardColor,
// Example mapping
errorColor: theme.colorScheme.error,
// Example mapping
bodyMediumStyle: theme.textTheme.bodyMedium,
labelMediumStyle: theme.inputDecorationTheme.hintStyle,
// Example mapping
bodySmallStyle: theme.textTheme.bodySmall,
// Pass the implemented backend interaction callbacks
sendChatDataToBackend: _sendChatDataToBackend,
handleEmbeddedWidgetCommand: _handleEmbeddedWidgetCommand,
),
),
// Optional: Add a canvas/side panel area here, managed by _ChatPageState state
// This area would display widgets triggered by 'show_canvas_widget' commands
// if (_showCanvas)
// Container(
// width: 300, // Example width
// color: Colors.grey[200],
// child: Center(child: Text('Canvas Area')), // Replace with dynamic widget loading
// // You would dynamically build the widget here based on _canvasWidgetLabel and _canvasWidgetParams
// // using your registry:
// // final registry = Provider.of<ib_conversations.EmbeddedWidgetRegistry>(context);
// // final builder = registry.getBuilder(_canvasWidgetLabel!);
// // if (builder != null) builder(context, _canvasWidgetParams ?? {}, _handleEmbeddedWidgetCommand)
// ),
],
),
),
);
}
}
// Helper extension for DateTime to match the simulated data display widget
extension DateTimeFormatting on DateTime {
String toShortString() {
return DateFormat('yyyy-MM-dd HH:mm').format(this);
}
}