refactor entire frontend

This commit is contained in:
Shivering Ocelot committed 2025-05-14 18:25:50 -05:00
1 parent 49df72f2a1
commit 7b3326524e
17 files changed
+545 -95

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
.venv .venv
.env
__pycache__ __pycache__
horde.db horde.db
+1 -1
View File
@@ -13,6 +13,6 @@ install: venv
web: frontend/build/web/index.html web: frontend/build/web/index.html
frontend/build/web/index.html: frontend/build/web/index.html:
cd frontend && flutter build web cd frontend && flutter build web --release --source-maps --no-web-resources-cdn
frontend/build/web/index.html: $(shell find frontend/lib -name '*.dart') frontend/build/web/index.html: $(shell find frontend/lib -name '*.dart')
+1
View File
@@ -209,6 +209,7 @@ class App:
user_message = { user_message = {
'action': 'chat-message', 'action': 'chat-message',
'id': message.id, 'id': message.id,
'chatId': message.chat_id,
'time': message.time, 'time': message.time,
'sender': message.sender, 'sender': message.sender,
'kind': message.kind, 'kind': message.kind,
+131
View File
@@ -0,0 +1,131 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/ws');
var nonceCounter = 1;
var nonceCallbacks = {};
var messageQueue = [];
var messageResolverQueue = [];
// Setup
ws.on('open', function open() {
const packet = JSON.stringify({ username: "test-user", password: "123456" });
ws.send(packet);
main();
});
ws.on('message', function incoming(rawMessage) {
const parsed = JSON.parse(rawMessage);
const callback = nonceCallbacks[parsed.nonce];
if (callback) {
callback(parsed);
} else {
if (messageResolverQueue.length > 0) {
const resolver = messageResolverQueue.shift();
resolver(parsed);
} else {
messageQueue.push(parsed);
}
}
});
async function send (action, message) {
const nonce = nonceCounter++;
let promise = new Promise((resolve, reject) => {
nonceCallbacks[nonce] = res => {
if (res.error) reject(Error(res.error));
else resolve(res);
};;
});
const packet = JSON.stringify({ nonce, action, ...message });
ws.send(packet);
return promise;
}
function waitMessage() {
return new Promise((resolve) => {
if (messageQueue.length > 0) {
resolve(messageQueue.shift());
} else {
messageResolverQueue.push(resolve);
}
});
}
// Testing
async function main() {
const prompt = process.argv[2]
let agents = (await send('get-agents')).agents;
console.log("Agents", agents);
console.log("Send prompt:", prompt)
let promptResponse = await send('send-agent-prompt', {
body: {
agentId: agents[0].id,
content: "Hello World!",
}
});
console.log("[AGENT]")
console.log(promptResponse);
}
async function runTest() {
console.log("Initial Chats",
await send('get-chats')
);
console.log("Create Agent",
await send('create-agent', {body: {
name: 'First Agent',
description: 'First agent does nothing',
kind: 'simple',
data: {}
}})
);
let agents = (await send('get-agents')).agents;
console.log("Get Agents", agents);
if (USE_LLM) {
let promptResponse = await send('send-agent-prompt', {
body: {
agentId: agents[0].id,
content: "Hello World!",
}
});
console.log("Prompt Response", promptResponse);
}
await testChats();
process.exit();
}
async function testChats () {
let response = await send('get-chats')
let chat = response.chats[0];
if (USE_LLM) {
let msgRes = await send('send-chat-message', {
body: {
chatId: chat.id,
content: "Whats up!",
}
});
console.log('message sent', msgRes);
console.log('Incoming:', await waitMessage());
// Multi turn conversation
await send('send-chat-message', {
body: {
chatId: chat.id,
content: "Spell the initials of my first message.",
}
});
console.log('Incoming:', await waitMessage());
}
}
-9
View File
@@ -1,9 +0,0 @@
import 'chat.dart';
class Agent {
void receivedChatMessage(Chat chat, String message) {
// Simple response from the agent
String responseMessage = "Agent received: " + message;
chat.addAgentMessage(responseMessage);
}
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'chat_list_screen.dart';
class AppMenu extends StatelessWidget {
const AppMenu({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Menu')),
body: ListView(
children: <Widget>[
ListTile(
title: const Text('Chats'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChatListScreen(),
),
);
},
),
// Add other settings options here
],
),
);
}
}
+71 -7
View File
@@ -1,27 +1,31 @@
import 'dart:html'; import 'dart:html';
import 'dart:convert'; import 'dart:convert';
import 'dart:async'; // Import for handling Completers
class Backend { class Backend {
static WebSocket? _socket; static WebSocket? _socket;
static String? _url; static String? _url;
static String? _username;
static Map<int, Completer> _pendingActions = {};
static Map<int, StreamController<Map<String, dynamic>>> _chatControllers = {};
static int _nextNonce = 0; // Tracking nonce for each action
static StreamController<Map<String, dynamic>> _packetStreamController = StreamController.broadcast();
static void setup(String url) { static void setup(String url) {
_url = url; _url = url;
connect(); connect();
} }
static void auth(username) { static void auth(String username, String password) {
_socket!.send( _socket!.send(json.encode({'username': username, 'password': password}));
json.encode({'username': username}) _username = username;
);
} }
static void connect() { static void connect() {
_socket = WebSocket(_url!); _socket = WebSocket(_url!);
_socket!.onOpen.listen((event) { _socket!.onOpen.listen((event) {
print('Connected to the WebSocket server'); print('Connected to the WebSocket server');
auth("the-user");
ping();
}); });
_socket!.onError.listen((event) { _socket!.onError.listen((event) {
print('Error: $event'); print('Error: $event');
@@ -31,6 +35,9 @@ class Backend {
print('Disconnected. Attempting to reconnect...'); print('Disconnected. Attempting to reconnect...');
_reconnect(); _reconnect();
}); });
_socket!.onMessage.listen((event) {
_handleIncomingMessage(event.data);
});
} }
static void _reconnect() { static void _reconnect() {
@@ -39,8 +46,65 @@ class Backend {
}); });
} }
static Future<dynamic> sendAction(String action, Map<String, dynamic> body) {
int nonce = _nextNonce++;
Completer completer = Completer();
_pendingActions[nonce] = completer;
final packet = {
'action': action,
'nonce': nonce,
'body': body,
};
_socket!.send(json.encode(packet));
return completer.future.then((response) {
_pendingActions.remove(nonce); // Clean up the mapping
return response;
});
}
static void _handleIncomingMessage(String data) {
var jsonData = json.decode(data);
print("Incoming Packet: $jsonData");
if (jsonData['nonce'] != null) {
int nonce = jsonData['nonce'];
if (_pendingActions.containsKey(nonce)) {
// Complete the response
_pendingActions[nonce]!.complete(jsonData);
} else {
print("Orphan response for action nonce $nonce");
}
} else if (jsonData['action'] == 'chat-message') {
int chatId = jsonData['chatId'];
final controller = _chatControllers[chatId];
if (controller != null && !controller.isClosed) {
controller.add(jsonData);
} else {
print("No active listeners for chatId $chatId");
}
} else {
_packetStreamController.add(jsonData);
}
}
static Stream<Map<String, dynamic>> chatMessages(int chatId) {
if (!_chatControllers.containsKey(chatId) || _chatControllers[chatId]!.isClosed) {
_chatControllers[chatId] = StreamController<Map<String, dynamic>>.broadcast();
}
return _chatControllers[chatId]!.stream;
}
static void removeChatListener(int chatId) {
final controller = _chatControllers.remove(chatId);
if (controller != null && !controller.isClosed) {
controller.close();
}
}
static void ping() { static void ping() {
if (_socket != null && _socket!.readyState == WebSocket.OPEN) { if (_socket!= null && _socket!.readyState == WebSocket.OPEN) {
final data = json.encode({'path': 'ping'}); final data = json.encode({'path': 'ping'});
_socket!.send(data); _socket!.send(data);
print('Ping sent: $data'); print('Ping sent: $data');
-31
View File
@@ -1,31 +0,0 @@
import 'agent.dart'; // Import the Agent class
class Message {
final String content;
final String sender;
Message(this.content, this.sender);
}
class Chat {
final String id;
final List<Message> _messages = [];
final Agent agent = Agent(); // Create an instance of Agent
Chat(this.id);
List<Message> get messages => List.unmodifiable(_messages);
void addUserMessage(String message) {
_messages.add(Message(message, 'user'));
agent.receivedChatMessage(this, message); // Call agent's method
}
void addAgentMessage(String message) {
_messages.add(Message(message, 'agent'));
}
void clearMessages() {
_messages.clear();
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'backend.dart';
import 'chat_screen.dart';
class ChatEntry {
final int id;
final String name;
ChatEntry({required this.id, required this.name});
}
class ChatListScreen extends StatelessWidget {
const ChatListScreen({super.key});
static Future<List<ChatEntry>> getChats() async {
final response = await Backend.sendAction("get-chats", {});
List<dynamic> chatObjects = response['chats'];
List<ChatEntry> chats = chatObjects
.map((msg) => ChatEntry(
id: msg['id'],
name: msg['name']?.isNotEmpty == true ? msg['name'] : '[Chat ${msg['id']}]',
))
.toList();
return chats;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Chats')),
body: FutureBuilder<List<ChatEntry>>(
future: getChats(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No chats available.'));
} else {
final chats = snapshot.data!;
return ListView.builder(
itemCount: chats.length,
itemBuilder: (context, index) {
final chat = chats[index];
return ListTile(
title: Text(chat.name),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(chatId: chat.id),
),
);
},
);
},
);
}
},
),
);
}
}
+112 -32
View File
@@ -1,56 +1,133 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'chat.dart'; // Import the new Chat class import 'app_menu.dart';
import 'styles.dart';
import 'backend.dart';
import 'dart:async';
class Message {
final String sender;
final String text;
final bool isFromUser;
Message({required this.sender, required this.text}) : isFromUser = sender == "user";
Alignment get alignment => isFromUser ? Alignment.centerRight : Alignment.centerLeft;
EdgeInsets get padding => isFromUser ? EdgeInsets.zero : const EdgeInsets.only(left: 10);
}
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
const ChatScreen({super.key}); final int chatId;
ChatScreen({required this.chatId});
static Future<List<Message>> getChatMessages(int chatId) async {
final response = await Backend.sendAction("get-chat-messages", {'chatId': chatId});
print("get-chat-messages response: $response");
List<dynamic> messageObjects = response['messages'];
List<Message> messages = messageObjects.map((msg) => Message(sender: msg['sender'], text: msg['content'] as String)).toList();
return messages;
}
@override @override
State<ChatScreen> createState() => _ChatScreenState(); _ChatScreenState createState() => _ChatScreenState();
} }
class _ChatScreenState extends State<ChatScreen> { class _ChatScreenState extends State<ChatScreen> {
final Chat _chat = Chat('chat1'); // Create an instance of Chat List<Message> _messages = [];
final TextEditingController _controller = TextEditingController(); final TextEditingController _textController = TextEditingController();
StreamSubscription? _chatSubscription;
void _sendMessage() { @override
if (_controller.text.isNotEmpty) { void initState() {
super.initState();
_loadInitialMessages();
_chatSubscription = Backend.chatMessages(widget.chatId).listen((data) {
final message = Message(sender: data['sender'], text: data['content']);
if (!message.isFromUser) {
addMessage(message);
}
});
}
@override
void dispose() {
Backend.removeChatListener(widget.chatId);
super.dispose();
}
Future<void> _loadInitialMessages() async {
final initialMessages = await ChatScreen.getChatMessages(widget.chatId);
setState(() { setState(() {
_chat.addUserMessage(_controller.text); // Use the Chat instance to add the message _messages.addAll(initialMessages);
_controller.clear(); });
}
void _handleNewMessage(Map<String, dynamic> message) {
final newMessage = Message(sender: message['sender'], text: message['content'] as String);
if (newMessage.sender!= "user") {
setState(() {
_messages.add(newMessage);
}); });
} }
} }
void addMessage(Message message) {
setState(() {
_messages.add(message);
});
}
void sendMessage(String content) {
_textController.clear();
final newMessage = Message(sender: "user", text: content);
Backend.sendAction("send-chat-message", {'chatId': widget.chatId, 'content': content}).then((response) {
if (response['error'] != null) {
print("Error sending chat message: ${response['error']}");
}
});
addMessage(newMessage);
}
Color getMessageColor(BuildContext context) {
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
final messageColor = Color.alphaBlend(Colors.white.withOpacity(0.1), backgroundColor);
return messageColor;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final isWideScreen = MediaQuery.of(context).size.width > 600;
final isBigScreen = width > 600; // Define threshold for small screens
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Chat'), title: Text('Chat Room ${widget.chatId}'),
actions: [
IconButton(icon: Icon(Icons.settings), onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => AppMenu()));
}),
],
), ),
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: _chat.messages.length, itemCount: _messages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = _chat.messages[index]; final message = _messages[index];
// Align all messages to left for big screens return Padding(
final align = message.sender == 'user' && !isBigScreen ? CrossAxisAlignment.end : CrossAxisAlignment.start; padding: isWideScreen ? message.padding : EdgeInsets.zero,
return Row( child: Align(
mainAxisAlignment: align == CrossAxisAlignment.end ? MainAxisAlignment.end : MainAxisAlignment.start, alignment: isWideScreen ? Alignment.centerLeft : message.alignment,
children: [ child: Container(
Container( margin: EdgeInsets.symmetric(vertical: 2, horizontal: 8),
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(10),
margin: const EdgeInsets.symmetric(vertical: 4.0), decoration: BoxDecoration(
child: Text( color: getMessageColor(context),
'${message.content}', borderRadius: BorderRadius.circular(12),
style: const TextStyle(color: Colors.white), ),
child: Text(message.text),
), ),
), ),
],
); );
}, },
), ),
@@ -61,14 +138,17 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
controller: _controller, controller: _textController,
decoration: const InputDecoration(hintText: 'Send a message'), decoration: inputDecoration(),
onSubmitted: (_) => _sendMessage(), onSubmitted: sendMessage, // Send message on Enter key
), ),
), ),
IconButton( SizedBox(width: 8),
icon: const Icon(Icons.send), ElevatedButton(
onPressed: _sendMessage, onPressed: () {
sendMessage(_textController.text);
},
child: Text('Send'),
), ),
], ],
), ),
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
class LoginScreen extends StatelessWidget {
final Function(String, String) onLogin;
LoginScreen({required this.onLogin});
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(controller: usernameController, decoration: InputDecoration(labelText: 'Username')),
SizedBox(height: 20),
TextField(
controller: passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true, // Hide password characters
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
onLogin(usernameController.text, passwordController.text);
},
child: Text('Login'),
),
],
),
),
),
);
}
}
+13 -10
View File
@@ -1,43 +1,46 @@
import 'dart:html' as html; import 'dart:html' as html;
import 'dart:io' as io; import 'dart:io' as io;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'chat_screen.dart';
import 'backend.dart'; import 'backend.dart';
import 'user_checker.dart';
class Config { class Config {
static String? backendUrl; // Declare it as nullable static String? backendUrl; // Declare it as nullable
static void loadConfig() { static void loadConfig() {
// Check the platform for web or other
if (html.window.location.origin.isNotEmpty) { if (html.window.location.origin.isNotEmpty) {
// For web applications // For web applications
backendUrl = html.window.location.origin; // Get the server URL from window.location backendUrl = html.window.location.origin; // Get the server URL from window.location
} else if (io.Platform.environment.containsKey('BACKEND_URL')) { }
// For Android or other platforms, check for the environment variable // For Android or other platforms, check for the environment variable
/*if (io.Platform.environment.containsKey('BACKEND_URL')) {
backendUrl = io.Platform.environment['BACKEND_URL']; backendUrl = io.Platform.environment['BACKEND_URL'];
} else if (html.window.location.origin.isNotEmpty) {
// For web applications
backendUrl = html.window.location.origin; // Get the server URL from window.location
} else { } else {
// If no value is determined, set it to null // If no value is determined, set it to null
backendUrl = null; backendUrl = null;
} }*/
} }
} }
void main() { void main() {
print("Main");
Config.loadConfig(); // Load the configuration before running the app Config.loadConfig(); // Load the configuration before running the app
Backend.setup('${Config.backendUrl}/ws'); Backend.setup('${Config.backendUrl}/ws');
runApp(const MyApp()); runApp(HubApp());
} }
class MyApp extends StatelessWidget { class HubApp extends StatelessWidget {
const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Chat', title: 'Horde Hub',
theme: ThemeData.dark(), theme: ThemeData.dark(),
home: const ChatScreen(), home: UserChecker(),
); );
} }
} }
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
InputDecoration inputDecoration() {
return InputDecoration(
border: OutlineInputBorder(),
hintText: 'Send a message',
);
}
+64
View File
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'backend.dart';
import 'login_screen.dart';
import 'chat_screen.dart';
import 'chat_list_screen.dart';
class UserChecker extends StatefulWidget {
@override
_UserCheckerState createState() => _UserCheckerState();
}
class _UserCheckerState extends State<UserChecker> {
String? username;
bool isLoading = false;
@override
void initState() {
super.initState();
username = null;
}
static void fetchChats(Function(List<int>) onSuccess) {
Backend.sendAction("get-chats", {}).then((response) {
print("chats response: $response");
List<dynamic> chatObjects = response['chats'];
List<int> chatIds = chatObjects.map((chat) => chat['id'] as int).toList();
onSuccess(chatIds);
});
}
void _handleLogin(String user, String password) {
Backend.auth(user, password);
setState(() {
username = user;
isLoading = true; // Show loading spinner
});
fetchChats((chatIds) {
if (chatIds.isEmpty) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => ChatListScreen()),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => ChatScreen(chatId: chatIds.first)),
);
}
});
}
@override
Widget build(BuildContext context) {
print("Build UserChecker: $username $isLoading");
if (isLoading) {
return Center(child: CircularProgressIndicator()); // Loading state UI
}
return LoginScreen(onLogin: _handleLogin);
}
}
+3
View File
@@ -13,6 +13,9 @@ case "$1" in
cd backend cd backend
python app.py ../test-data python app.py ../test-data
;; ;;
"cli")
node cli/main.js "$2"
;;
"test") "test")
# Setup test data directory # Setup test data directory
mkdir -p test-data mkdir -p test-data
+1 -4
View File
@@ -22,7 +22,4 @@ MVP-3A has configurable agents, both user and agents can create new agents, also
MVP-3C has multiple chats, users and agents can create chats, modify chat titles, and alter chat participants. MVP-3C has multiple chats, users and agents can create chats, modify chat titles, and alter chat participants.
MVP-3P is permissions. Modifying chats and agents both need a permission system. MVP-3P is permissions. Modifying chats and agents both need a permission system.
**Current Status**: Frontend and backend connect via websocket. Backend does nothing else. Frontend has basic chat classes and gui, and a basic agent class that repeats the user message. **Current Status**: Frontend and backend connect via websocket. Backend has usable chats and agents with direct prompts and chat messaging. Frontend features user login, manual and automatic chat selection, and live connection with backend chat.
# Next Steps
These must be determined by the current system and the next target, currently MVP.
+5 -1
View File
@@ -1,6 +1,6 @@
const WebSocket = require('ws'); const WebSocket = require('ws');
const USE_LLM = false; const USE_LLM = true;
const ws = new WebSocket('ws://localhost:5000/ws'); const ws = new WebSocket('ws://localhost:5000/ws');
var nonceCounter = 1; var nonceCounter = 1;
@@ -113,5 +113,9 @@ async function testChats () {
} }
}); });
console.log('Incoming:', await waitMessage()); console.log('Incoming:', await waitMessage());
console.log('Messages in Chat', chat.id, await send('get-chat-messages', {
body: {chatId: chat.id}
}));
} }
} }