ib_conversations 0.0.10-alpha copy "ib_conversations: ^0.0.10-alpha" to clipboard
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);
  }
}