sendMessageStream method

Stream<String> sendMessageStream(
  1. String message
)

Sends a message to the AI service and returns a stream of responses.

This method enables real-time streaming of AI responses, allowing the UI to display responses as they are generated.

The message parameter is the user's input text.

Returns a Stream<String> that yields response chunks as they arrive.

Throws an Exception if:

  • API key is not set
  • Network request fails
  • API returns an error response

Example:

final stream = aiService.sendMessageStream('Tell me a story');
await for (final chunk in stream) {
  print(chunk); // Print each chunk as it arrives
}

Implementation

Stream<String> sendMessageStream(final String message) async* {
  if (apiKey == null || apiKey!.isEmpty) {
    throw Exception('API key not set. Please configure your OpenAI API key.');
  }

  try {
    final request = http.Request(
      'POST',
      Uri.parse('$_baseUrl/chat/completions'),
    );

    request.headers['Content-Type'] = 'application/json';
    request.headers['Authorization'] = 'Bearer $apiKey';

    request.body = jsonEncode({
      'model': _model,
      'messages': [
        {'role': 'user', 'content': message},
      ],
      'max_tokens': 1000,
      'temperature': 0.7,
      'stream': true,
    });

    final streamedResponse = await request.send();

    if (streamedResponse.statusCode == 200) {
      await for (final chunk in streamedResponse.stream.transform(
        utf8.decoder,
      )) {
        final lines = chunk.split('\n');
        for (final line in lines) {
          if (line.startsWith('data: ')) {
            final data = line.substring(6);
            if (data == '[DONE]') {
              break;
            }

            try {
              final json = jsonDecode(data) as Map<String, dynamic>;
              final choices = json['choices'] as List<dynamic>?;
              if (choices != null && choices.isNotEmpty) {
                final firstChoice = choices[0] as Map<String, dynamic>;
                final delta = firstChoice['delta'] as Map<String, dynamic>?;
                final content = delta?['content'] as String?;
                if (content != null) {
                  yield content;
                }
              }
            } on FormatException {
              // Skip malformed JSON chunks
              continue;
            }
          }
        }
      }
    } else {
      final errorResponse = await streamedResponse.stream.bytesToString();
      try {
        final error = jsonDecode(errorResponse) as Map<String, dynamic>;
        final errorObj = error['error'] as Map<String, dynamic>?;
        final errorMessage =
            (errorObj?['message'] as String?) ?? 'Unknown API error';

        // Provide more user-friendly error messages
        var userMessage = errorMessage;
        final messageLower = errorMessage.toLowerCase();
        if (messageLower.contains('quota') ||
            messageLower.contains('billing')) {
          userMessage =
              'API Quota Exceeded: $errorMessage\n\nPlease check your '
              'OpenAI account billing and usage limits.';
        } else if (messageLower.contains('invalid_api_key') ||
            messageLower.contains('authentication')) {
          userMessage =
              'Invalid API Key: Please check your OpenAI API key in '
              'settings.';
        } else if (messageLower.contains('rate_limit')) {
          userMessage =
              'Rate Limit Exceeded: Please wait a moment and try again.';
        }

        throw Exception(userMessage);
      } on FormatException {
        throw Exception(
          'API returned an invalid response. Status code: '
          '${streamedResponse.statusCode}',
        );
      }
    }
  } on FormatException catch (e) {
    throw Exception('Invalid response format: $e');
  } on Exception {
    rethrow;
  } catch (e) {
    throw Exception('Network error: $e');
  }
}