handleBotReply method

  1. @override
Future<void> handleBotReply(
  1. String roomId,
  2. List<Message> messages
)
override

Implementation

@override
Future<void> handleBotReply(String roomId, List<Message> messages) async {
  if (!botEnabled || messages.isEmpty) return;

  final r = room(roomId);
  if (r.isEmpty) return;

  final bot = _resolveBotProfile(r);
  if (bot == null) return;

  // ── Guard: leave in progress or cooldown ──
  if (_leaveInProgress.contains(roomId)) return;
  if (_isOnCooldown(roomId)) return;

  final incoming = _latestHumanMessage(messages, bot.id);
  if (incoming == null || incoming.type == MessageType.custom) return;

  // ── Guard: first entry — snapshot current latest message, skip reply ──
  // This prevents the bot from replying to messages that existed before
  // it joined this session (e.g. after app restart or room re-entry).
  if (!_initializedRooms.contains(roomId)) {
    _initializedRooms.add(roomId);
    final hasHumanHistory = messages.any((m) {
      return m.id != incoming.id &&
          m.senderId != bot.id &&
          m.type != MessageType.custom &&
          !m.isEmpty &&
          !m.shouldRemove;
    });

    if (hasHumanHistory) {
      _repliedMessageId[roomId] = incoming.id;
      return;
    }
  }

  if (_repliedMessageId[roomId] == incoming.id) return;
  if (_inFlight.contains(roomId)) return;

  _inFlight.add(roomId);
  try {
    // Mark bot online & schedule auto-leave
    await _setBotOnline(bot.id, true);
    _scheduleAutoLeave(roomId, bot);

    // ── 1. Simulate reading ──────────────────────────────────────────
    await _readDelay(incoming);

    // ── 2. Wait if partner is typing ─────────────────────────────────
    if (_isHumanTyping(roomId, bot.id)) {
      final newMessageArrived = await _waitHumanTyping(
        roomId,
        bot.id,
        incoming.id,
      );
      if (newMessageArrived) {
        // A fresh message landed — the listener will fire a new
        // handleBotReply call, so we bail out here.
        return;
      }
      // Partner typed but didn't send. Continue with original message.
    }

    // ── 3. Think pause ───────────────────────────────────────────────
    await _thinkDelay();

    // ── 4. Typing ON → generate → typing OFF → send ─────────────────
    final request = _buildRequest(bot, messages, incoming);
    if (request == null) return;

    await _markSeenByBot(roomId, bot.id, incoming);
    await _setBotTyping(roomId, bot.id, true);

    final reply = await aiDelegate.generate(request);

    await _setBotTyping(roomId, bot.id, false);

    if (reply == null || reply.trim().isEmpty) return;

    _repliedMessageId[roomId] = incoming.id;
    await _sendBotReply(roomId, bot.id, reply.trim());
  } catch (e, st) {
    errorReporter.report(
      e,
      stackTrace: st,
      source: 'BotResponseMixin.handleBotReply',
      context: {'roomId': roomId, 'incomingMsgId': incoming.id},
    );
    await _setBotTyping(roomId, bot.id, false);
  } finally {
    _inFlight.remove(roomId);
  }
}