- 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>
146 lines
3.7 KiB
Dart
146 lines
3.7 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../models/message.dart';
|
|
import '../services/chat_service.dart';
|
|
import '../providers/auth_provider.dart';
|
|
|
|
class ChatProvider with ChangeNotifier {
|
|
final ChatService _chatService = ChatService();
|
|
final List<Message> _messages = [];
|
|
|
|
AuthProvider? _authProvider;
|
|
StreamSubscription? _messageSubscription;
|
|
StreamSubscription? _stateSubscription;
|
|
|
|
String? _currentConversationId;
|
|
bool _isTyping = false;
|
|
String _streamingContent = '';
|
|
Message? _streamingMessage;
|
|
|
|
List<Message> get messages => List.unmodifiable(_messages);
|
|
bool get isTyping => _isTyping;
|
|
bool get isConnected => _chatService.currentState == ChatConnectionState.connected;
|
|
ChatConnectionState get connectionState => _chatService.currentState;
|
|
String? get currentConversationId => _currentConversationId;
|
|
|
|
void updateAuth(AuthProvider auth) {
|
|
_authProvider = auth;
|
|
_chatService.setToken(auth.token);
|
|
|
|
if (auth.isAuthenticated && !isConnected) {
|
|
connect();
|
|
} else if (!auth.isAuthenticated && isConnected) {
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
Future<void> connect() async {
|
|
if (_authProvider?.token == null) return;
|
|
|
|
_chatService.setToken(_authProvider!.token);
|
|
|
|
_stateSubscription?.cancel();
|
|
_stateSubscription = _chatService.connectionState.listen((state) {
|
|
notifyListeners();
|
|
});
|
|
|
|
_messageSubscription?.cancel();
|
|
_messageSubscription = _chatService.messages.listen(_handleMessage);
|
|
|
|
await _chatService.connect();
|
|
}
|
|
|
|
void _handleMessage(Map<String, dynamic> data) {
|
|
final type = data['type'];
|
|
|
|
switch (type) {
|
|
case 'connected':
|
|
notifyListeners();
|
|
break;
|
|
|
|
case 'start':
|
|
_currentConversationId = data['conversation_id'];
|
|
_isTyping = true;
|
|
_streamingContent = '';
|
|
_streamingMessage = Message(
|
|
role: 'assistant',
|
|
content: '',
|
|
isStreaming: true,
|
|
);
|
|
_messages.add(_streamingMessage!);
|
|
notifyListeners();
|
|
break;
|
|
|
|
case 'delta':
|
|
_streamingContent += data['content'] ?? '';
|
|
if (_streamingMessage != null) {
|
|
final index = _messages.indexOf(_streamingMessage!);
|
|
if (index >= 0) {
|
|
_messages[index] = _streamingMessage!.copyWith(
|
|
content: _streamingContent,
|
|
);
|
|
}
|
|
}
|
|
notifyListeners();
|
|
break;
|
|
|
|
case 'done':
|
|
_isTyping = false;
|
|
if (_streamingMessage != null) {
|
|
final index = _messages.indexOf(_streamingMessage!);
|
|
if (index >= 0) {
|
|
_messages[index] = _streamingMessage!.copyWith(
|
|
content: data['content'] ?? _streamingContent,
|
|
isStreaming: false,
|
|
);
|
|
}
|
|
}
|
|
_streamingMessage = null;
|
|
_streamingContent = '';
|
|
notifyListeners();
|
|
break;
|
|
|
|
case 'error':
|
|
_isTyping = false;
|
|
_messages.add(Message(
|
|
role: 'assistant',
|
|
content: 'Error: ${data['message']}',
|
|
));
|
|
notifyListeners();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void sendMessage(String content, {List<String>? files}) {
|
|
if (content.trim().isEmpty) return;
|
|
|
|
_messages.add(Message(
|
|
role: 'user',
|
|
content: content,
|
|
attachments: files,
|
|
));
|
|
notifyListeners();
|
|
|
|
_chatService.sendMessage(content, files: files);
|
|
}
|
|
|
|
void clearMessages() {
|
|
_messages.clear();
|
|
_currentConversationId = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void disconnect() {
|
|
_chatService.disconnect();
|
|
_messageSubscription?.cancel();
|
|
_stateSubscription?.cancel();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
disconnect();
|
|
_chatService.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|