Advanced chat management via UI

This commit is contained in:
Shivering Ocelot committed 2025-05-22 07:48:36 -05:00
1 parent fbee31841d
commit 5351144edf
9 files changed
+396 -59

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'backend.dart';
import 'chat_screen.dart';
import 'create_chat_screen.dart';
class AgentEntry {
final int id;
final String name;
AgentEntry({required this.id, required this.name});
}
class AgentListScreen extends StatelessWidget {
const AgentListScreen({super.key});
static Future<List<AgentEntry>> getChats() async {
final response = await Backend.sendAction("get-agents", {});
List<dynamic> chatObjects = response['agents'];
List<AgentEntry> chats = chatObjects
.map((msg) => AgentEntry(
id: msg['id'],
name: msg['name']?.isNotEmpty == true ? msg['name'] : '[Agent ${msg['id']}]',
))
.toList();
return chats;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Agents')),
body: Column( // Use a Column to stack widgets vertically
children: [
ListTile(
leading: const Icon(Icons.add),
title: const Text('Create'),
onTap: () {
print("TODO: Create new agents");
},
),
const Divider(height: 1),
Expanded(
child: FutureBuilder<List<AgentEntry>>(
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 agents available.'));
} else {
final agents = snapshot.data!;
return ListView.builder(
itemCount: agents.length,
itemBuilder: (context, index) {
final agent = agents[index];
return ListTile(
title: Text(agent.name),
onTap: () {
print("Navigate to agent ${agent.id}");
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(agentId: agent.id),
),
);*/
},
);
},
);
}
},
),
),
],
),
);
}
}
+12
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'agent_list_screen.dart';
import 'chat_list_screen.dart';
import 'user_checker.dart';
import 'backend.dart';
@@ -13,6 +14,17 @@ class AppMenu extends StatelessWidget {
appBar: AppBar(title: const Text('Menu')),
body: ListView(
children: <Widget>[
ListTile(
title: const Text('Agents'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AgentListScreen(),
),
);
},
),
ListTile(
title: const Text('Chats'),
onTap: () {
+39
View File
@@ -3,6 +3,13 @@ import 'dart:html';
import 'dart:convert';
import 'dart:async'; // Import for handling Completers
class ChatInfo {
int id;
String name;
ChatInfo({required this.id, required this.name});
}
class Backend {
static WebSocket? _socket;
static String? _url;
@@ -10,6 +17,9 @@ class Backend {
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();
// Cached Data
static List<ChatInfo>? _chats;
// Authentication
static ({String username, String password})? _credentials;
@@ -36,6 +46,8 @@ class Backend {
_socket = WebSocket(_url!);
_socket!.onOpen.listen((event) {
print('Connected to the WebSocket server');
Backend.invalidateCache(chat: true);
print('Logging in...');
if (_credentials != null) {
_socket!.send(json.encode({'username': _credentials!.username, 'password': _credentials!.password}));
@@ -128,6 +140,33 @@ class Backend {
}
}
static Future<List<ChatInfo>> getChats () async {
if (_chats == null) {
final response = await Backend.sendAction("get-chats", {});
List<dynamic> chatObjects = response['chats'];
_chats = chatObjects
.map((msg) => ChatInfo(
id: msg['id'],
name: msg['name']?.isNotEmpty == true ? msg['name'] : '[Chat ${msg['id']}]',
))
.toList();
}
return _chats!;
}
static Future<ChatInfo?> getChat (int chatId) async {
final chats = await getChats();
try {
return chats.firstWhere((chat) => chat.id == chatId);
} on StateError {
return null;
}
}
static void invalidateCache ({bool chat = false}) {
if (chat) { _chats = null; }
}
static void ping() {
if (_socket!= null && _socket!.readyState == WebSocket.OPEN) {
final data = json.encode({'path': 'ping'});
+88 -48
View File
@@ -1,63 +1,103 @@
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});
}
import 'create_chat_screen.dart';
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),
),
);
},
);
body: Column( // Use a Column to stack widgets vertically
children: [
ListTile(
leading: const Icon(Icons.add),
title: const Text('Create'),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => CreateChatScreen()));
},
),
const Divider(height: 1),
Expanded(
child: FutureBuilder<List<ChatInfo>>(
future: Backend.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),
),
);
},
trailing: Row(
mainAxisSize: MainAxisSize.min, // Important to make the row take minimum space
children: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
print('Config button pressed for chat: ${chat.name}');
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete Chat'),
content: Text('Are you sure you want to delete "${chat.name}"?'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Delete'),
onPressed: () async {
await Backend.sendAction('delete-chat', {'chatId': chat.id});
Backend.invalidateCache(chat: true);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const ChatListScreen()),
);
},
),
],
);
},
);
},
),
],
),
);
},
);
}
},
);
}
},
),
),
],
),
);
}
+6 -3
View File
@@ -33,6 +33,7 @@ class ChatScreen extends StatefulWidget {
}
class _ChatScreenState extends State<ChatScreen> {
String? _title;
List<Message> _messages = [];
final TextEditingController _textController = TextEditingController();
StreamSubscription? _chatSubscription;
@@ -40,7 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void initState() {
super.initState();
_loadInitialMessages();
_loadInitialData();
_chatSubscription = Backend.chatMessages(widget.chatId).listen((data) {
final message = Message(sender: data['sender'], text: data['content']);
if (!message.isFromUser) {
@@ -55,10 +56,12 @@ class _ChatScreenState extends State<ChatScreen> {
super.dispose();
}
Future<void> _loadInitialMessages() async {
Future<void> _loadInitialData() async {
final chatInfo = (await Backend.getChat(widget.chatId))!;
final initialMessages = await ChatScreen.getChatMessages(widget.chatId);
setState(() {
_messages.addAll(initialMessages);
_title = chatInfo.name;
});
}
@@ -100,7 +103,7 @@ class _ChatScreenState extends State<ChatScreen> {
return Scaffold(
appBar: AppBar(
title: Text('Chat Room ${widget.chatId}'),
title: Text(_title ?? 'Chat Room ${widget.chatId}'),
actions: [
IconButton(icon: Icon(Icons.settings), onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => AppMenu()));
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'backend.dart';
import 'chat_screen.dart';
class AgentEntry {
final int id;
final String name;
AgentEntry({required this.id, required this.name});
}
class CreateChatScreen extends StatefulWidget {
@override
_CreateChatScreenState createState() => _CreateChatScreenState();
}
class _CreateChatScreenState extends State<CreateChatScreen> {
final TextEditingController nameController = TextEditingController();
// Track selected participants
final Set<int> selectedParticipants = {};
void _submit() async {
Backend.invalidateCache(chat: true);
final response = await Backend.sendAction("create-chat", {
'name': nameController.text,
'members': selectedParticipants.toList(),
});
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(chatId: response['chatId']),
),
);
}
void _toggleParticipant(int id) {
setState(() {
if (selectedParticipants.contains(id)) {
selectedParticipants.remove(id);
} else {
selectedParticipants.add(id);
}
});
}
// TODO: Agents should only be retrieved once, then the same future is reused in `build`
static Future<List<AgentEntry>> getAgents() async {
final response = await Backend.sendAction("get-agents", {});
List<dynamic> chatObjects = response['agents'];
List<AgentEntry> chats = chatObjects
.map((msg) => AgentEntry(
id: msg['id'],
name: msg['name']?.isNotEmpty == true ? msg['name'] : '[Chat ${msg['id']}]',
))
.toList();
return chats;
}
Widget _buildParticipantTile(AgentEntry entry) {
final isSelected = selectedParticipants.contains(entry.id);
return GestureDetector(
onTap: () => _toggleParticipant(entry.id),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
margin: EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color: isSelected ? Colors.blue.shade100 : Colors.grey.shade200,
border: Border.all(
color: isSelected ? Colors.blue : Colors.transparent,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
entry.name,
style: TextStyle(
fontWeight: FontWeight.w600,
color: isSelected ? Colors.blue.shade900 : Colors.black87,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Create Chat')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: nameController,
decoration: InputDecoration(labelText: 'Title'),
),
SizedBox(height: 20),
Align(
alignment: Alignment.centerLeft,
child: Text('Select Participants:',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold)),
),
SizedBox(height: 8),
Expanded(
child: FutureBuilder<List<AgentEntry>>(
future: getAgents(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final agents = snapshot.data!;
return ListView.builder(
itemCount: agents.length,
itemBuilder: (context, index) {
return _buildParticipantTile(agents[index]);
},
);
} else {
return ListView.builder(
itemCount: 0,
itemBuilder: (context, index) {
return ListTile();
},
);
}
},
),
),
ElevatedButton(
onPressed: _submit,
child: Text('Create Chat'),
),
],
),
),
),
);
}
}