-
Notifications
You must be signed in to change notification settings - Fork 549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update Gemini sample #2846
base: main
Are you sure you want to change the base?
Update Gemini sample #2846
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,8 +52,8 @@ class _ChatScreenState extends State<ChatScreen> { | |
body: switch (apiKey) { | ||
final providedKey? => ChatWidget(apiKey: providedKey), | ||
_ => ApiKeyWidget(onSubmitted: (key) { | ||
setState(() => apiKey = key); | ||
}), | ||
setState(() => apiKey = key); | ||
}), | ||
}, | ||
); | ||
} | ||
|
@@ -75,8 +75,8 @@ class ApiKeyWidget extends StatelessWidget { | |
children: [ | ||
const Text( | ||
'To use the Gemini API, you\'ll need an API key. ' | ||
'If you don\'t already have one, ' | ||
'create a key in Google AI Studio.', | ||
'If you don\'t already have one, ' | ||
'create a key in Google AI Studio.', | ||
), | ||
const SizedBox(height: 8), | ||
Link( | ||
|
@@ -93,7 +93,7 @@ class ApiKeyWidget extends StatelessWidget { | |
Expanded( | ||
child: TextField( | ||
decoration: | ||
textFieldDecoration(context, 'Enter your API key'), | ||
textFieldDecoration(context, 'Enter your API key'), | ||
controller: _textController, | ||
onSubmitted: (value) { | ||
onSubmitted(value); | ||
|
@@ -117,9 +117,11 @@ class ApiKeyWidget extends StatelessWidget { | |
} | ||
|
||
class ChatWidget extends StatefulWidget { | ||
const ChatWidget({required this.apiKey, super.key}); | ||
|
||
final String apiKey; | ||
const ChatWidget({ | ||
required this.apiKey, | ||
super.key, | ||
}); | ||
|
||
@override | ||
State<ChatWidget> createState() => _ChatWidgetState(); | ||
|
@@ -130,7 +132,7 @@ class _ChatWidgetState extends State<ChatWidget> { | |
late final ChatSession _chat; | ||
final ScrollController _scrollController = ScrollController(); | ||
final TextEditingController _textController = TextEditingController(); | ||
final FocusNode _textFieldFocus = FocusNode(debugLabel: 'TextField'); | ||
final FocusNode _textFieldFocus = FocusNode(); | ||
bool _loading = false; | ||
|
||
@override | ||
|
@@ -145,7 +147,7 @@ class _ChatWidgetState extends State<ChatWidget> { | |
|
||
void _scrollDown() { | ||
WidgetsBinding.instance.addPostFrameCallback( | ||
(_) => _scrollController.animateTo( | ||
(_) => _scrollController.animateTo( | ||
_scrollController.position.maxScrollExtent, | ||
duration: const Duration( | ||
milliseconds: 750, | ||
|
@@ -157,19 +159,40 @@ class _ChatWidgetState extends State<ChatWidget> { | |
|
||
@override | ||
Widget build(BuildContext context) { | ||
final history = _chat.history.toList(); | ||
var textFieldDecoration = InputDecoration( | ||
contentPadding: const EdgeInsets.all(15), | ||
hintText: 'Enter a prompt...', | ||
border: OutlineInputBorder( | ||
borderRadius: const BorderRadius.all( | ||
Radius.circular(14), | ||
), | ||
borderSide: BorderSide( | ||
color: Theme.of(context).colorScheme.secondary, | ||
), | ||
), | ||
focusedBorder: OutlineInputBorder( | ||
borderRadius: const BorderRadius.all( | ||
Radius.circular(14), | ||
), | ||
borderSide: BorderSide( | ||
color: Theme.of(context).colorScheme.secondary, | ||
), | ||
), | ||
); | ||
|
||
return Padding( | ||
padding: const EdgeInsets.all(8.0), | ||
child: Column( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
crossAxisAlignment: CrossAxisAlignment.start, | ||
children: [ | ||
Expanded( | ||
child: ListView.builder( | ||
child: widget.apiKey.isNotEmpty | ||
? ListView.builder( | ||
controller: _scrollController, | ||
itemBuilder: (context, idx) { | ||
final content = history[idx]; | ||
final text = content.parts | ||
var content = _chat.history.toList()[idx]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a note, I changed this piece intentionally to avoid recreating the same list, and potentially reevaluating the |
||
var text = content.parts | ||
.whereType<TextPart>() | ||
.map<String>((e) => e.text) | ||
.join(''); | ||
|
@@ -178,7 +201,12 @@ class _ChatWidgetState extends State<ChatWidget> { | |
isFromUser: content.role == 'user', | ||
); | ||
}, | ||
itemCount: history.length, | ||
itemCount: _chat.history.length, | ||
) | ||
: ListView( | ||
children: const [ | ||
Text('No API key found. Please provide an API Key.'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point to add this, since I think the login(?) page currently allows empty submissions. We don't have a back button, so maybe we should say to restart the sample? However, it's hopefully a rare mistake, so it might be fine to not provide special details or handling to avoid diverging too much from the original sample. |
||
], | ||
), | ||
), | ||
Padding( | ||
|
@@ -192,15 +220,16 @@ class _ChatWidgetState extends State<ChatWidget> { | |
child: TextField( | ||
autofocus: true, | ||
focusNode: _textFieldFocus, | ||
decoration: | ||
textFieldDecoration(context, 'Enter a prompt...'), | ||
decoration: textFieldDecoration, | ||
controller: _textController, | ||
onSubmitted: (String value) { | ||
_sendChatMessage(value); | ||
}, | ||
), | ||
), | ||
const SizedBox.square(dimension: 15), | ||
const SizedBox.square( | ||
dimension: 15, | ||
), | ||
if (!_loading) | ||
IconButton( | ||
onPressed: () async { | ||
|
@@ -227,13 +256,13 @@ class _ChatWidgetState extends State<ChatWidget> { | |
}); | ||
|
||
try { | ||
final response = await _chat.sendMessage( | ||
var response = await _chat.sendMessage( | ||
Content.text(message), | ||
); | ||
final text = response.text; | ||
var text = response.text; | ||
|
||
if (text == null) { | ||
_showError('Empty response.'); | ||
_showError('No response from API.'); | ||
return; | ||
} else { | ||
setState(() { | ||
|
@@ -262,7 +291,7 @@ class _ChatWidgetState extends State<ChatWidget> { | |
return AlertDialog( | ||
title: const Text('Something went wrong'), | ||
content: SingleChildScrollView( | ||
child: Text(message), | ||
child: SelectableText(message), | ||
), | ||
actions: [ | ||
TextButton( | ||
|
@@ -279,24 +308,24 @@ class _ChatWidgetState extends State<ChatWidget> { | |
} | ||
|
||
class MessageWidget extends StatelessWidget { | ||
final String text; | ||
final bool isFromUser; | ||
|
||
const MessageWidget({ | ||
super.key, | ||
required this.text, | ||
required this.isFromUser, | ||
}); | ||
|
||
final String text; | ||
final bool isFromUser; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Row( | ||
mainAxisAlignment: | ||
isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start, | ||
isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start, | ||
children: [ | ||
Flexible( | ||
child: Container( | ||
constraints: const BoxConstraints(maxWidth: 480), | ||
constraints: const BoxConstraints(maxWidth: 600), | ||
decoration: BoxDecoration( | ||
color: isFromUser | ||
? Theme.of(context).colorScheme.primaryContainer | ||
|
@@ -308,7 +337,10 @@ class MessageWidget extends StatelessWidget { | |
horizontal: 20, | ||
), | ||
margin: const EdgeInsets.only(bottom: 8), | ||
child: MarkdownBody(data: text), | ||
child: MarkdownBody( | ||
selectable: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Woo, I was wanting this yesterday! Nice that the package makes it so easy :) |
||
data: text, | ||
), | ||
), | ||
), | ||
], | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the same as the
textFieldDecoration
method you added below to share with the API screen? Do you want to keep sharing that implementation or nah?