Files
ARCHITECT f199daf4ba Change PIN to 1451
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 23:31:52 +00:00

224 lines
5.6 KiB
Dart

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<String, dynamic> 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<Map<String, dynamic>>.broadcast();
final _stateController = StreamController<ChatConnectionState>.broadcast();
final _errorController = StreamController<String>.broadcast();
ChatConnectionState _currentState = ChatConnectionState.disconnected;
int _reconnectAttempts = 0;
Timer? _reconnectTimer;
Timer? _pingTimer;
bool _intentionalDisconnect = false;
Stream<Map<String, dynamic>> get messages => _messagesController.stream;
Stream<ChatConnectionState> get connectionState => _stateController.stream;
Stream<String> get errors => _errorController.stream;
ChatConnectionState get currentState => _currentState;
void setToken(String? token) {
_token = token;
}
void _setState(ChatConnectionState state) {
_currentState = state;
_stateController.add(state);
}
Future<void> 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<String, dynamic> 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<String, dynamic> 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();
}
}