import 'dart:async'; import 'dart:convert'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../config/api_config.dart'; enum ChatConnectionState { disconnected, connecting, connected, reconnecting, error, } /// Chat session info (v3 - dynamic sessions) class ChatSession { final String sessionId; final String name; final String? createdAt; ChatSession({ required this.sessionId, required this.name, this.createdAt, }); factory ChatSession.fromJson(Map json) { return ChatSession( sessionId: json['session_id'] ?? '', name: json['name'] ?? 'Session', createdAt: json['created_at'], ); } } /// Chat service that connects to Claude sessions (v3) class ChatService { WebSocketChannel? _channel; String? _token; final _messagesController = StreamController>.broadcast(); final _stateController = StreamController.broadcast(); final _errorController = StreamController.broadcast(); ChatConnectionState _currentState = ChatConnectionState.disconnected; int _reconnectAttempts = 0; Timer? _reconnectTimer; Timer? _pingTimer; bool _intentionalDisconnect = false; Stream> get messages => _messagesController.stream; Stream get connectionState => _stateController.stream; Stream get errors => _errorController.stream; ChatConnectionState get currentState => _currentState; void setToken(String? token) { _token = token; } void _setState(ChatConnectionState state) { _currentState = state; _stateController.add(state); } Future connect() async { if (_token == null) return; if (_currentState == ChatConnectionState.connecting) return; _intentionalDisconnect = false; _setState(ChatConnectionState.connecting); try { _channel?.sink.close(); _channel = WebSocketChannel.connect( Uri.parse('${ApiConfig.wsUrl}/ws/chat'), ); // Send auth token immediately _channel!.sink.add(jsonEncode({'token': _token})); _channel!.stream.listen( (data) { try { final message = jsonDecode(data); _handleMessage(message); } catch (e) { _errorController.add('Failed to parse message: $e'); } }, onError: (error) { _errorController.add('WebSocket error: $error'); _setState(ChatConnectionState.error); if (!_intentionalDisconnect) { _scheduleReconnect(); } }, onDone: () { _setState(ChatConnectionState.disconnected); if (!_intentionalDisconnect) { _scheduleReconnect(); } }, cancelOnError: false, ); } catch (e) { _errorController.add('Connection failed: $e'); _setState(ChatConnectionState.error); if (!_intentionalDisconnect) { _scheduleReconnect(); } } } void _handleMessage(Map message) { final type = message['type']; switch (type) { case 'init': // Initial state with sessions list _setState(ChatConnectionState.connected); _reconnectAttempts = 0; _startPingTimer(); _messagesController.add(message); break; case 'error': final errorMsg = message['message'] ?? message['content'] ?? 'Unknown error'; _errorController.add(errorMsg); break; case 'pong': break; default: // Forward all other messages (output, done, session_connected, etc.) _messagesController.add(message); } } void _startPingTimer() { _pingTimer?.cancel(); _pingTimer = Timer.periodic(const Duration(seconds: 30), (_) { if (_currentState == ChatConnectionState.connected) { sendRaw({'type': 'ping'}); } }); } void _scheduleReconnect() { if (_intentionalDisconnect) return; if (_reconnectAttempts >= ApiConfig.maxReconnectAttempts) { _errorController.add('Max reconnection attempts reached'); return; } _reconnectTimer?.cancel(); final delay = Duration( seconds: ApiConfig.reconnectDelay.inSeconds * (1 << _reconnectAttempts), ); _reconnectAttempts++; _setState(ChatConnectionState.reconnecting); _reconnectTimer = Timer(delay, () { if (_token != null && !_intentionalDisconnect) { connect(); } }); } /// Create a new chat session void createSession(String name) { sendRaw({ 'type': 'create_session', 'name': name, }); } /// Connect to an existing session by session_id void connectToSession(String sessionId) { sendRaw({ 'type': 'connect_session', 'session_id': sessionId, }); } /// Send message to connected session void sendMessage(String content) { if (_currentState != ChatConnectionState.connected) { _errorController.add('Not connected'); return; } sendRaw({ 'type': 'message', 'content': content, }); } /// Request sessions list void listSessions() { sendRaw({'type': 'list_sessions'}); } void sendRaw(Map data) { if (_channel != null) { _channel!.sink.add(jsonEncode(data)); } } void disconnect() { _intentionalDisconnect = true; _pingTimer?.cancel(); _reconnectTimer?.cancel(); _channel?.sink.close(); _setState(ChatConnectionState.disconnected); } void dispose() { disconnect(); _messagesController.close(); _stateController.close(); _errorController.close(); } }