Skip to content

Commit

Permalink
Add model selector and add some hints to empty state.
Browse files Browse the repository at this point in the history
  • Loading branch information
danopato committed Jul 13, 2024
1 parent e76964a commit 4a28314
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 25 deletions.
3 changes: 3 additions & 0 deletions gai-frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ app.*.map.json
/android/app/profile
/android/app/release
deploy-pat.sh

set_providers.sh

192 changes: 171 additions & 21 deletions gai-frontend/lib/chat/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ import 'package:orchid/orchid/orchid.dart';
import 'package:orchid/api/orchid_crypto.dart';
import 'package:orchid/orchid/orchid_gradients.dart';
import 'package:orchid/orchid/orchid_titled_panel.dart';

import 'package:flutter/gestures.dart';
import 'package:url_launcher/url_launcher.dart';

import 'chat_bubble.dart';
import 'chat_button.dart';
import 'chat_message.dart';
import '../provider.dart';
import 'chat_prompt.dart';
import 'chat_model_button.dart';
import '../config/providers_config.dart';

class ChatView extends StatefulWidget {
const ChatView({super.key});
Expand All @@ -29,7 +35,9 @@ class ChatView extends StatefulWidget {

class _ChatViewState extends State<ChatView> {
List<ChatMessage> _messages = [];
List<String> _providers = [];
// List<String> _providers = [];
// Map<String, Map<String, String>> _providers = {'gpt4': {'url': 'https://nanogenera.danopato.com/ws/', 'name': 'ChatGPT-4'}};
late final Map<String, Map<String, String>> _providers;
int _providerIndex = 0;
bool _debugMode = false;
bool _connected = false;
Expand All @@ -51,6 +59,7 @@ class _ChatViewState extends State<ChatView> {
@override
void initState() {
super.initState();
_providers = ProvidersConfig.getProviders();
_bidController.value = _bid;
try {
_initFromParams();
Expand All @@ -59,6 +68,13 @@ class _ChatViewState extends State<ChatView> {
}
}

bool _emptyState() {
if (_account != null || _connected) {
return false;
}
return true;
}

Account? get _account {
if (_funder == null || _signerKey == null) {
return null;
Expand Down Expand Up @@ -106,41 +122,58 @@ class _ChatViewState extends State<ChatView> {
_accountChanged();
String? provider = params['provider'];
if (provider != null) {
_providers = [provider];
_providers = {'user-provider': {'url': provider, 'name': 'User Provider'}};
}
}

void providerConnected() {
void providerConnected([name = '']) {
String nameTag = '';
_connected = true;
addMessage(ChatMessageSource.system, 'Connected to provider.');
if (!name.isEmpty) {
nameTag = ' ${name}';
}
addMessage(ChatMessageSource.system, 'Connected to provider${nameTag}.');
}

void providerDisconnected() {
_connected = false;
addMessage(ChatMessageSource.system, 'Provider disconnected');
}

void _connectProvider() {
void _connectProvider([provider = '']) {
var account = _accountDetail;
String url;
String name;
String providerId = '';
if (account == null) {
return;
}
if (_providers.length == 0) {
log('_connectProvider() -- _providers.length == 0');
return;
}
if (_connected) {
_providerConnection?.dispose();
_providerIndex = (_providerIndex + 1) % _providers.length;
_connected = false;
}
log('Connecting to provider: ${_providers[_providerIndex]}');
if (provider.isEmpty) {
_providerIndex += 1;
providerId = _providers.keys.elementAt(_providerIndex);
} else {
providerId = provider;
}
url = _providers[providerId]?['url'] ?? '';
name = _providers[providerId]?['name'] ?? '';

log('Connecting to provider: ${name}');
_providerConnection = ProviderConnection(
onMessage: (msg) {
addMessage(ChatMessageSource.internal, msg);
},
onConnect: providerConnected,
onConnect: () { providerConnected(name); },
onChat: (msg, metadata) {
addMessage(ChatMessageSource.provider, msg, metadata: metadata);
addMessage(ChatMessageSource.provider, msg, metadata: metadata, sourceName: name);
},
onDisconnect: providerDisconnected,
onError: (msg) {
Expand All @@ -155,16 +188,20 @@ class _ChatViewState extends State<ChatView> {
accountDetail: account,
contract:
EthereumAddress.from('0x6dB8381b2B41b74E17F5D4eB82E8d5b04ddA0a82'),
url: _providers[_providerIndex],
url: url,
);
log('connected...');
}

void addMessage(ChatMessageSource source, String msg,
{Map<String, dynamic>? metadata}) {
{Map<String, dynamic>? metadata, String sourceName = ''}) {
log('Adding message: ${msg.truncate(64)}');
setState(() {
_messages.add(ChatMessage(source, msg, metadata: metadata));
if (sourceName.isEmpty) {
_messages.add(ChatMessage(source, msg, metadata: metadata));
} else {
_messages.add(ChatMessage(source, msg, metadata: metadata, sourceName: sourceName));
}
});
// if (source != ChatMessageSource.internal || _debugMode == true) {
scrollMessagesDown();
Expand Down Expand Up @@ -249,6 +286,11 @@ class _ChatViewState extends State<ChatView> {
).top(16),
// Account card
AccountCard(accountDetail: _accountDetail).top(20),
ChatButton(
onPressed: () => _launchURL('https://account.orchid.com'),
text: 'Manage Account',
width: 200,
).top(20),
],
).pad(24),
);
Expand Down Expand Up @@ -347,17 +389,11 @@ class _ChatViewState extends State<ChatView> {
fit: BoxFit.scaleDown,
child: SizedBox(
width: minWidth,
child: _buildHeaderRow(showIcons: showIcons)))
child: _buildHeaderRow(showIcons: showIcons, providers: _providers)))
else
_buildHeaderRow(showIcons: showIcons),
_buildHeaderRow(showIcons: showIcons, providers: _providers),
// Messages area
Flexible(
child: ListView.builder(
controller: messageListController,
itemCount: _messages.length,
itemBuilder: _buildChatBubble,
).top(16),
),
_buildChatPane(),
// Prompt row
AnimatedSize(
alignment: Alignment.topCenter,
Expand All @@ -383,16 +419,89 @@ class _ChatViewState extends State<ChatView> {
);
}

Widget _buildHeaderRow({required bool showIcons}) {
Widget _buildChatPane() {
return Flexible(
child: Stack(
children: <Widget>[
ListView.builder(
controller: messageListController,
itemCount: _messages.length,
itemBuilder: _buildChatBubble,
).top(16),
if (_emptyState())
Positioned(
top: 35, // Adjust this value to align with the Account button
right: 0,
child: CustomPaint(
painter: CalloutPainter(),
child: Container(
width: 390,
padding: EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'This is a demonstration of the application of Orchid Nanopayments within a consolidated Multi-LLM chat service.',
style: OrchidText.normal_14.copyWith(color: Colors.white),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
Text(
'To get started, enter or create a funded Orchid account.',
style: OrchidText.normal_14.copyWith(color: Colors.white),
textAlign: TextAlign.center,
),
ChatButton(
text: 'Enter Account',
onPressed: _popAccountDialog,
width: 200,
).top(24),
SizedBox(height: 16),
OutlinedButton(
onPressed: () => _launchURL('https://account.orchid.com'),
child: Text('Create Account').button,
style: OutlinedButton.styleFrom(
side: BorderSide(color: Theme.of(context).primaryColor),
minimumSize: Size(200, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
),
SizedBox(height: 24),
InkWell(
onTap: () => _launchURL('https://docs.orchid.com/en/latest/accounts/'),
child: Text(
'Learn more about creating an Orchid account',
style: TextStyle(color: Colors.blue[300], decoration: TextDecoration.underline),
),
),
],
),
),
),
),
],
),
);
}

Widget _buildHeaderRow({required bool showIcons, required Map<String, Map<String, String>> providers}) {
return Row(
children: <Widget>[
SizedBox(height: 40, child: OrchidAsset.image.logo),
const Spacer(),
// Connect button
ChatModelButton(
updateModel: (id) { log(id); _connectProvider(id); },
providers: providers,
).left(8),
/*
ChatButton(
text: 'Reroll',
onPressed: _connectProvider,
).left(8),
*/
// Clear button
ChatButton(text: 'Clear Chat', onPressed: _clearChat).left(8),
// Account button
Expand All @@ -411,6 +520,47 @@ class _ChatViewState extends State<ChatView> {
}
}

class CalloutPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.8)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;

final radius = 10.0; // Corner radius
final calloutWidth = 25.0;
final calloutHeight = 20.0;
final calloutStart = size.width - 115.0; // Adjust this to position the callout

final path = Path()
..moveTo(radius, 0)
..lineTo(calloutStart, 0)
..lineTo(calloutStart + (calloutWidth / 2), -calloutHeight)
..lineTo(calloutStart + calloutWidth, 0)
..lineTo(size.width - radius, 0)
..quadraticBezierTo(size.width, 0, size.width, radius)
..lineTo(size.width, size.height - radius)
..quadraticBezierTo(size.width, size.height, size.width - radius, size.height)
..lineTo(radius, size.height)
..quadraticBezierTo(0, size.height, 0, size.height - radius)
..lineTo(0, radius)
..quadraticBezierTo(0, 0, radius, 0);

canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Future<void> _launchURL(String urlString) async {
final Uri url = Uri.parse(urlString);
if (!await launchUrl(url)) {
throw 'Could not launch $url';
}
}

// Rename this in a subclass as prelude to refactoring later.
class TransientEthereumKey extends StoredEthereumKey {
TransientEthereumKey({required super.imported, required super.private});
Expand Down
19 changes: 17 additions & 2 deletions gai-frontend/lib/chat/chat_bubble.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ChatBubble extends StatelessWidget {
),
);
}

return Align(
alignment: src == ChatMessageSource.provider
? Alignment.centerLeft
Expand All @@ -59,8 +60,9 @@ class ChatBubble extends StatelessWidget {
children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: Text(src == ChatMessageSource.provider ? 'Chat' : 'You',
style: OrchidText.normal_14),
child: _chatSourceText(message),
// child: Text(src == ChatMessageSource.provider ? 'Chat' : 'You',
// style: OrchidText.normal_14),
),
const SizedBox(height: 2),
ClipRRect(
Expand Down Expand Up @@ -101,5 +103,18 @@ class ChatBubble extends StatelessWidget {
),
);
}

Widget _chatSourceText(ChatMessage msg) {
final String srcText;
if (msg.sourceName.isEmpty) {
srcText = msg.source == ChatMessageSource.provider ? 'Chat' : 'You';
} else {
srcText = msg.sourceName;
}
return Text(
srcText,
style: OrchidText.normal_14,
);
}
}

6 changes: 5 additions & 1 deletion gai-frontend/lib/chat/chat_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ class ChatButton extends StatelessWidget {
super.key,
required this.text,
required this.onPressed,
this.width,
this.height = 40,
});

final String text;
final VoidCallback onPressed;
final double? width, height;

@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
height: height,
width: width,
child: FilledButton(
style: TextButton.styleFrom(
backgroundColor: OrchidColors.new_purple,
Expand Down
3 changes: 2 additions & 1 deletion gai-frontend/lib/chat/chat_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ enum ChatMessageSource { client, provider, system, internal }

class ChatMessage {
final ChatMessageSource source;
final String sourceName;
final String msg;
final Map<String, dynamic>? metadata;

ChatMessage(this.source, this.msg, {this.metadata});
ChatMessage(this.source, this.msg, {this.metadata, this.sourceName = ''});

String get message {
return msg;
Expand Down
Loading

0 comments on commit 4a28314

Please sign in to comment.