Files
social-app/apps/lib/features/chat/presentation/bloc/chat_bloc.dart
T

219 lines
6.1 KiB
Dart
Raw Normal View History

import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:social_app/core/chat/chat_api.dart';
import 'package:social_app/core/chat/agent_stage.dart';
import 'package:social_app/core/chat/ag_ui_event.dart';
import 'package:social_app/core/chat/ag_ui_service.dart';
import 'package:social_app/core/chat/chat_list_item.dart';
import 'package:social_app/core/chat/chat_orchestrator.dart';
import 'package:social_app/core/chat/chat_history_repository.dart';
import 'package:social_app/core/chat/chat_timeline_reconciler.dart';
import 'package:social_app/core/l10n/l10n.dart';
import 'chat_bloc_recovery_utils.dart';
part 'chat_bloc_events.dart';
part 'chat_bloc_send.dart';
part 'chat_bloc_history.dart';
part 'chat_bloc_attachments.dart';
class ChatState implements ChatOrchestratorState {
@override
final List<ChatListItem> items;
@override
final bool isSending;
@override
final bool isWaitingFirstToken;
@override
final bool isStreaming;
@override
final bool isCancelling;
@override
final bool isLoadingHistory;
@override
final String? currentMessageId;
@override
final String? error;
@override
final DateTime? oldestLoadedDate;
@override
final bool hasEarlierHistory;
@override
final AgentStage? currentStage;
final bool hasSeenStep;
const ChatState({
this.items = const [],
this.isSending = false,
this.isWaitingFirstToken = false,
this.isStreaming = false,
this.isCancelling = false,
this.isLoadingHistory = false,
this.currentMessageId,
this.error,
this.oldestLoadedDate,
this.hasEarlierHistory = false,
this.currentStage,
this.hasSeenStep = false,
});
@override
bool get isLoading =>
isSending ||
isWaitingFirstToken ||
isStreaming ||
isCancelling ||
isLoadingHistory;
static const _unset = Object();
ChatState copyWith({
List<ChatListItem>? items,
bool? isSending,
bool? isWaitingFirstToken,
bool? isStreaming,
bool? isCancelling,
bool? isLoadingHistory,
Object? currentMessageId = _unset,
Object? error = _unset,
Object? oldestLoadedDate = _unset,
bool? hasEarlierHistory,
Object? currentStage = _unset,
bool? hasSeenStep,
}) {
return ChatState(
items: items ?? this.items,
isSending: isSending ?? this.isSending,
isWaitingFirstToken: isWaitingFirstToken ?? this.isWaitingFirstToken,
isStreaming: isStreaming ?? this.isStreaming,
isCancelling: isCancelling ?? this.isCancelling,
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
currentMessageId: currentMessageId == _unset
? this.currentMessageId
: currentMessageId as String?,
error: error == _unset ? this.error : error as String?,
oldestLoadedDate: oldestLoadedDate == _unset
? this.oldestLoadedDate
: oldestLoadedDate as DateTime?,
hasEarlierHistory: hasEarlierHistory ?? this.hasEarlierHistory,
currentStage: currentStage == _unset
? this.currentStage
: currentStage as AgentStage?,
hasSeenStep: hasSeenStep ?? this.hasSeenStep,
);
}
}
class ChatBloc extends Cubit<ChatState> implements ChatOrchestrator {
ChatBloc({
AgUiService? service,
required ChatApi chatApi,
ChatHistoryRepository? historyRepository,
Duration recoveryPollInterval = const Duration(milliseconds: 700),
Duration recoveryTimeout = const Duration(seconds: 20),
}) : _service =
service ??
AgUiService(chatApi: chatApi, historyRepository: historyRepository),
_recoveryPollInterval = recoveryPollInterval,
_recoveryTimeout = recoveryTimeout,
super(const ChatState()) {
_service.onEvent = _handleEvent;
}
2026-03-16 16:11:28 +08:00
final AgUiService _service;
final Duration _recoveryPollInterval;
final Duration _recoveryTimeout;
String? _activeUserId;
int _sessionEpoch = 0;
2026-03-16 16:11:28 +08:00
final Map<String, Uint8List> _attachmentPreviewCache = <String, Uint8List>{};
final Map<String, Future<Uint8List?>> _attachmentPreviewInflight =
<String, Future<Uint8List?>>{};
/// Common state reset for run completion (success/error/cancel)
ChatState _resetRunState({String? error, String? currentMessageId}) {
return state.copyWith(
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
currentMessageId: currentMessageId,
error: error,
currentStage: null,
hasSeenStep: false,
);
}
@override
Future<void> sendMessage(String content, {List<XFile>? images}) {
return _sendMessage(content, images: images);
2026-03-17 15:58:29 +08:00
}
@override
Future<void> loadHistory() {
return _loadHistory();
}
@override
Future<void> loadMoreHistory() {
return _loadMoreHistory();
}
@override
Future<String> transcribeAudioFile(String filePath) {
return _service.transcribeAudio(filePath);
}
@override
Future<bool> cancelCurrentRun() async {
if (!(state.isWaitingFirstToken ||
state.isStreaming ||
state.isCancelling)) {
return false;
}
emit(state.copyWith(isCancelling: true, error: null));
try {
await _service.cancelCurrentRun();
emit(
state.copyWith(
isSending: false,
isWaitingFirstToken: false,
isStreaming: false,
isCancelling: false,
currentMessageId: null,
),
);
return true;
} catch (error) {
emit(state.copyWith(isCancelling: false, error: error.toString()));
return false;
}
}
@override
void clearError() {
emit(state.copyWith(error: null));
}
Future<Uint8List?> loadAttachmentPreview(String previewPath) {
return _loadAttachmentPreview(previewPath);
}
Future<void> switchUser(String? userId) async {
final normalizedUserId = userId?.trim();
if (_activeUserId == normalizedUserId) {
return;
}
final epoch = ++_sessionEpoch;
_activeUserId = normalizedUserId;
await _service.setUserContext(normalizedUserId);
if (epoch != _sessionEpoch) {
return;
}
_attachmentPreviewCache.clear();
_attachmentPreviewInflight.clear();
emit(const ChatState());
}
}