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