diff --git a/backend/agent.py b/backend/agent.py index dd2debb..9cfdfb8 100644 --- a/backend/agent.py +++ b/backend/agent.py @@ -34,8 +34,6 @@ class Agent: ] tools = [] - # while this constructor accepts two parameters, - # all subclasses must also accept a fourth 'data' parameter def __init__(self, app, agent_id, data): self.app = app self.agent_id = agent_id diff --git a/backend/app.py b/backend/app.py index 21e2964..65beeac 100644 --- a/backend/app.py +++ b/backend/app.py @@ -31,7 +31,7 @@ class App: if replicate_api_token: self.replicate_client = replicate.Client(api_token=replicate_api_token) else: - raise Exception("Horde only works with a replicate client. Please provide an API Token.") + raise Exception("Horde only works with a replicate connection. Please provide an API Token.") # `Agent` is the kind 'basic', will be class `BasicAgent` in the future self.agent_classes = [Agent] @@ -119,7 +119,7 @@ class App: if data: # Assign a copy to trigger a value change - agent.data = agent.data.copy() + agent.data = agent.data.copy() if agent.data else {} agent.data.update(data) summary.extend([f"data.{name}" for name in data.keys()]) @@ -335,8 +335,6 @@ if __name__ == "__main__": print("No directory specified. Using current directory as default.") data_directory = "." - print("api token", os.getenv('REPLICATE_API_TOKEN')) - app = App( data_directory=data_directory, replicate_api_token=os.getenv('REPLICATE_API_TOKEN'), diff --git a/backend/server.py b/backend/server.py index 2472bdb..605c632 100644 --- a/backend/server.py +++ b/backend/server.py @@ -86,6 +86,10 @@ def start_server(app): await websocket.send(json.dumps({'error': 'invalid credentials'})) return + # TODO: print username instead, same in the finally block + print("User logged in:", user_id) + await websocket.send(json.dumps({'message': 'authentication successful'})) + send_queue = asyncio.Queue() async def sender_task(): nonlocal send_queue @@ -190,6 +194,7 @@ def start_server(app): else: await send({'error': 'unknown action: ' + action, 'nonce': nonce}) finally: + print("User logged out:", user_id) app.remove_user_connection(user_id, send_queue) server.run(debug=True) \ No newline at end of file diff --git a/frontend/lib/backend.dart b/frontend/lib/backend.dart index 66896e2..0562d67 100644 --- a/frontend/lib/backend.dart +++ b/frontend/lib/backend.dart @@ -24,7 +24,7 @@ class Backend { // Authentication static ({String username, String password})? _credentials; - static Completer? _loginCompleter; + static Completer? _loginCompleter; static Completer? _credentialsCompleter; static void setup(String url) { @@ -49,8 +49,11 @@ class Backend { } } - static Future auth(String username, String password) { - _socket!.send(json.encode({'username': username, 'password': password})); + static Future auth(String username, String password) { + if (_credentials != null) { + print("Cannot authenticate twice"); + return Future.value(false); + } _credentials = (username: username, password: password); if (_credentialsCompleter != null) { _credentialsCompleter!.complete(_credentials); @@ -62,11 +65,13 @@ class Backend { static void connect() { _socket = WebSocket(_url! + "/ws"); - _socket!.onOpen.listen((event) { + _socket!.onOpen.listen((event) async { print('Connected to the WebSocket server'); Backend.invalidateCache(chat: true); print('Logging in...'); + // secure the listener for the response before sending credentials + var responseFuture = nextIncomingMessage(); if (_credentials != null) { _socket!.send(json.encode({'username': _credentials!.username, 'password': _credentials!.password})); } else { @@ -75,7 +80,14 @@ class Backend { _socket!.send(json.encode({'username': credentials.username, 'password': credentials.password})); }); } - _loginCompleter?.complete(); + var response = await responseFuture; + if (response['error'] == null) { + _loginCompleter?.complete(true); + } else { + _loginCompleter?.complete(false); + _socket!.close(); // Retry + } + _loginCompleter = null; }); _socket!.onError.listen((event) { print('Error: $event'); @@ -92,11 +104,10 @@ class Backend { static void disconnect() { _socket?.close(); - _loginCompleter = null; } static void _reconnect() { - Future.delayed(Duration(seconds: 5), () { + Future.delayed(Duration(seconds: 1), () { connect(); }); } @@ -140,10 +151,17 @@ class Backend { print("No active listeners for chatId $chatId"); } } else { + if (jsonData['error'] != null) { + print("Error: ${jsonData['error']}"); + } _packetStreamController.add(jsonData); } } + static Future> nextIncomingMessage() { + return _packetStreamController.stream.first; + } + static Stream> chatMessages(int chatId) { if (!_chatControllers.containsKey(chatId) || _chatControllers[chatId]!.isClosed) { _chatControllers[chatId] = StreamController>.broadcast(); diff --git a/frontend/lib/chat_screen.dart b/frontend/lib/chat_screen.dart index 7a0c3dd..7dd96ec 100644 --- a/frontend/lib/chat_screen.dart +++ b/frontend/lib/chat_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'app_menu.dart'; import 'styles.dart'; import 'backend.dart'; @@ -36,6 +37,7 @@ class _ChatScreenState extends State { String? _title; List _messages = []; final TextEditingController _textController = TextEditingController(); + final FocusNode _inputFocusNode = FocusNode(); StreamSubscription? _chatSubscription; @override @@ -137,23 +139,36 @@ class _ChatScreenState extends State { ), Padding( padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _textController, - decoration: inputDecoration(), - onSubmitted: sendMessage, // Send message on Enter key + child: KeyboardListener( + focusNode: _inputFocusNode, + onKeyEvent: (event) { + // TODO: Probably doing this in the input focus node would be cleaner + if (HardwareKeyboard.instance.isControlPressed && HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.enter)) { + sendMessage(_textController.text); + _textController.clear(); + //return KeyEventResult.handled; + } + //return KeyEventResult.ignored; + }, + child: Column( + children: [ + Expanded( + child: TextField( + controller: _textController, + maxLines: null, + decoration: inputDecoration(), + ), ), - ), - SizedBox(width: 8), - ElevatedButton( - onPressed: () { - sendMessage(_textController.text); - }, - child: Text('Send'), - ), - ], + SizedBox(width: 8), + ElevatedButton( + onPressed: () { + sendMessage(_textController.text); + _textController.clear(); + }, + child: Text('Send'), + ), + ], + ), ), ), ], diff --git a/frontend/lib/user_checker.dart b/frontend/lib/user_checker.dart index b8e79ab..2ef67ff 100644 --- a/frontend/lib/user_checker.dart +++ b/frontend/lib/user_checker.dart @@ -44,12 +44,18 @@ class _UserCheckerState extends State { } void _handleLogin(String user, String password) async { + bool success = await Backend.auth(user, password); + if (!success) { + setState(() { + isLoading = false; + }); + return; + } + setState(() { username = user; isLoading = true; }); - - await Backend.auth(user, password); await _storage.write(key: 'username', value: user); await _storage.write(key: 'password', value: password);