- Flutter app with chat and terminal screens - WebSocket integration for real-time chat - xterm integration for screen sessions - Markdown rendering with code blocks - JWT authentication Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
106 lines
2.6 KiB
Dart
106 lines
2.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,
|
|
error,
|
|
}
|
|
|
|
class ChatService {
|
|
WebSocketChannel? _channel;
|
|
String? _token;
|
|
|
|
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
|
final _stateController = StreamController<ChatConnectionState>.broadcast();
|
|
|
|
Stream<Map<String, dynamic>> get messages => _messageController.stream;
|
|
Stream<ChatConnectionState> get connectionState => _stateController.stream;
|
|
|
|
ChatConnectionState _currentState = ChatConnectionState.disconnected;
|
|
ChatConnectionState get currentState => _currentState;
|
|
|
|
void setToken(String? token) {
|
|
_token = token;
|
|
}
|
|
|
|
Future<void> connect() async {
|
|
if (_token == null) {
|
|
_updateState(ChatConnectionState.error);
|
|
return;
|
|
}
|
|
|
|
_updateState(ChatConnectionState.connecting);
|
|
|
|
try {
|
|
final uri = Uri.parse('${ApiConfig.wsUrl}${ApiConfig.wsChat}');
|
|
_channel = WebSocketChannel.connect(uri);
|
|
|
|
await _channel!.ready;
|
|
|
|
// Send auth token
|
|
_channel!.sink.add(jsonEncode({'token': _token}));
|
|
|
|
_channel!.stream.listen(
|
|
(data) {
|
|
try {
|
|
final message = jsonDecode(data);
|
|
if (message['type'] == 'connected') {
|
|
_updateState(ChatConnectionState.connected);
|
|
}
|
|
_messageController.add(message);
|
|
} catch (e) {
|
|
// Handle parse error
|
|
}
|
|
},
|
|
onError: (error) {
|
|
_updateState(ChatConnectionState.error);
|
|
},
|
|
onDone: () {
|
|
_updateState(ChatConnectionState.disconnected);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
_updateState(ChatConnectionState.error);
|
|
}
|
|
}
|
|
|
|
void sendMessage(String content, {List<String>? files}) {
|
|
if (_channel == null || _currentState != ChatConnectionState.connected) {
|
|
return;
|
|
}
|
|
|
|
_channel!.sink.add(jsonEncode({
|
|
'type': 'message',
|
|
'content': content,
|
|
'files': files ?? [],
|
|
}));
|
|
}
|
|
|
|
void ping() {
|
|
if (_channel != null && _currentState == ChatConnectionState.connected) {
|
|
_channel!.sink.add(jsonEncode({'type': 'ping'}));
|
|
}
|
|
}
|
|
|
|
void _updateState(ChatConnectionState state) {
|
|
_currentState = state;
|
|
_stateController.add(state);
|
|
}
|
|
|
|
void disconnect() {
|
|
_channel?.sink.close();
|
|
_channel = null;
|
|
_updateState(ChatConnectionState.disconnected);
|
|
}
|
|
|
|
void dispose() {
|
|
disconnect();
|
|
_messageController.close();
|
|
_stateController.close();
|
|
}
|
|
}
|