Created
May 13, 2025 20:56
-
-
Save devoncarew/915fb37cb26377ee7b0d2c887c1bd80e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2024 the Dart project authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style license | |
// that can be found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
import 'package:flutter_markdown/flutter_markdown.dart'; | |
import 'package:google_generative_ai/google_generative_ai.dart'; | |
import 'package:url_launcher/link.dart'; | |
void main() { | |
runApp(const GenerativeAISample()); | |
} | |
class GenerativeAISample extends StatelessWidget { | |
const GenerativeAISample({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
title: 'Flutter + Generative AI', | |
theme: ThemeData( | |
colorScheme: ColorScheme.fromSeed( | |
brightness: Brightness.dark, | |
seedColor: const Color.fromARGB(255, 171, 222, 244), | |
), | |
useMaterial3: true, | |
), | |
home: const ChatScreen(title: 'Flutter + Generative AI'), | |
); | |
} | |
} | |
class ChatScreen extends StatefulWidget { | |
const ChatScreen({super.key, required this.title}); | |
final String title; | |
@override | |
State<ChatScreen> createState() => _ChatScreenState(); | |
} | |
class _ChatScreenState extends State<ChatScreen> { | |
String? apiKey; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(title: Text(widget.title)), | |
body: switch (apiKey) { | |
final providedKey? => ChatWidget(apiKey: providedKey), | |
_ => ApiKeyWidget( | |
onSubmitted: (key) { | |
setState(() => apiKey = key); | |
}, | |
), | |
}, | |
); | |
} | |
} | |
class ApiKeyWidget extends StatelessWidget { | |
ApiKeyWidget({required this.onSubmitted, super.key}); | |
final ValueChanged<String> onSubmitted; | |
final TextEditingController _textController = TextEditingController(); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
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.', | |
), | |
const SizedBox(height: 8), | |
Link( | |
uri: Uri.https('makersuite.google.com', '/app/apikey'), | |
target: LinkTarget.blank, | |
builder: | |
(context, followLink) => TextButton( | |
onPressed: followLink, | |
child: const Text('Get an API Key'), | |
), | |
), | |
const SizedBox(height: 8), | |
Row( | |
children: [ | |
Expanded( | |
child: TextField( | |
decoration: textFieldDecoration( | |
context, | |
'Enter your API key', | |
), | |
controller: _textController, | |
onSubmitted: (value) { | |
onSubmitted(value); | |
}, | |
), | |
), | |
const SizedBox(height: 8), | |
TextButton( | |
onPressed: () { | |
onSubmitted(_textController.value.text); | |
}, | |
child: const Text('Submit'), | |
), | |
], | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ChatWidget extends StatefulWidget { | |
const ChatWidget({required this.apiKey, super.key}); | |
final String apiKey; | |
@override | |
State<ChatWidget> createState() => _ChatWidgetState(); | |
} | |
class _ChatWidgetState extends State<ChatWidget> { | |
late final GenerativeModel _model; | |
late final ChatSession _chat; | |
final ScrollController _scrollController = ScrollController(); | |
final TextEditingController _textController = TextEditingController(); | |
final FocusNode _textFieldFocus = FocusNode(debugLabel: 'TextField'); | |
bool _loading = false; | |
@override | |
void initState() { | |
super.initState(); | |
_model = GenerativeModel(model: 'gemini-pro', apiKey: widget.apiKey); | |
_chat = _model.startChat(); | |
} | |
void _scrollDown() { | |
WidgetsBinding.instance.addPostFrameCallback( | |
(_) => _scrollController.animateTo( | |
_scrollController.position.maxScrollExtent, | |
duration: const Duration(milliseconds: 750), | |
curve: Curves.easeOutCirc, | |
), | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final history = _chat.history.toList(); | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Expanded( | |
child: ListView.builder( | |
controller: _scrollController, | |
itemBuilder: (context, idx) { | |
final content = history[idx]; | |
final text = content.parts | |
.whereType<TextPart>() | |
.map<String>((e) => e.text) | |
.join(''); | |
return MessageWidget( | |
text: text, | |
isFromUser: content.role == 'user', | |
); | |
}, | |
itemCount: history.length, | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15), | |
child: Row( | |
children: [ | |
Expanded( | |
child: TextField( | |
autofocus: true, | |
focusNode: _textFieldFocus, | |
decoration: textFieldDecoration( | |
context, | |
'Enter a prompt...', | |
), | |
controller: _textController, | |
onSubmitted: (String value) { | |
_sendChatMessage(value); | |
}, | |
), | |
), | |
const SizedBox.square(dimension: 15), | |
if (!_loading) | |
IconButton( | |
onPressed: () async { | |
_sendChatMessage(_textController.text); | |
}, | |
icon: Icon( | |
Icons.send, | |
color: Theme.of(context).colorScheme.primary, | |
), | |
) | |
else | |
const CircularProgressIndicator(), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
Future<void> _sendChatMessage(String message) async { | |
setState(() { | |
_loading = true; | |
}); | |
try { | |
final response = await _chat.sendMessage(Content.text(message)); | |
final text = response.text; | |
if (text == null) { | |
_showError('Empty response.'); | |
return; | |
} else { | |
setState(() { | |
_loading = false; | |
_scrollDown(); | |
}); | |
} | |
} catch (e) { | |
_showError(e.toString()); | |
setState(() { | |
_loading = false; | |
}); | |
} finally { | |
_textController.clear(); | |
setState(() { | |
_loading = false; | |
}); | |
_textFieldFocus.requestFocus(); | |
} | |
} | |
void _showError(String message) { | |
showDialog<void>( | |
context: context, | |
builder: (context) { | |
return AlertDialog( | |
title: const Text('Something went wrong'), | |
content: SingleChildScrollView(child: Text(message)), | |
actions: [ | |
TextButton( | |
onPressed: () { | |
Navigator.of(context).pop(); | |
}, | |
child: const Text('OK'), | |
), | |
], | |
); | |
}, | |
); | |
} | |
} | |
class MessageWidget extends StatelessWidget { | |
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, | |
children: [ | |
Flexible( | |
child: Container( | |
constraints: const BoxConstraints(maxWidth: 480), | |
decoration: BoxDecoration( | |
color: | |
isFromUser | |
? Theme.of(context).colorScheme.primaryContainer | |
: Theme.of(context).colorScheme.surfaceContainerHighest, | |
borderRadius: BorderRadius.circular(18), | |
), | |
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20), | |
margin: const EdgeInsets.only(bottom: 8), | |
child: MarkdownBody(data: text), | |
), | |
), | |
], | |
); | |
} | |
} | |
InputDecoration textFieldDecoration(BuildContext context, String hintText) => | |
InputDecoration( | |
contentPadding: const EdgeInsets.all(15), | |
hintText: hintText, | |
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), | |
), | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment