Skip to content

Commit 452c838

Browse files
authored
updating the flutter AI playground's behavior and layout (#52)
2 parents 4f5bc1d + d07d4a5 commit 452c838

26 files changed

+495
-577
lines changed

firebase_ai_logic_showcase/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ ephemeral/
5151
#firebase
5252
google-services.json
5353
GoogleService-Info.plist
54+
firebase.json
5455
.firebaserc
-203 KB
Loading

firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart

Lines changed: 28 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
2222
import 'package:image_picker/image_picker.dart';
2323
import '../../shared/ui/app_frame.dart';
2424
import '../../shared/ui/app_spacing.dart';
25-
import './ui_components/ui_components.dart';
26-
import './firebaseai_chat_service.dart';
27-
import 'ui_components/model_picker.dart';
28-
import './models/models.dart';
25+
import '../../shared/ui/chat_components/ui_components.dart';
26+
import '../../shared/chat_service.dart';
27+
import '../../shared/models/models.dart';
2928

3029
class ChatDemo extends ConsumerStatefulWidget {
3130
const ChatDemo({super.key});
@@ -45,18 +44,15 @@ class _ChatDemoState extends ConsumerState<ChatDemo> {
4544
Uint8List? _attachment;
4645
final ScrollController _scrollController = ScrollController();
4746
bool _loading = false;
48-
OverlayPortalController opController = OverlayPortalController();
4947

5048
@override
5149
void initState() {
5250
super.initState();
53-
_chatService = ChatService(ref);
51+
final model = geminiModels.selectModel('gemini-2.5-flash');
52+
_chatService = ChatService(ref, model);
5453
_chatService.init();
55-
_userTextInputController.text = geminiModels.selectedModel.defaultPrompt;
56-
57-
WidgetsBinding.instance.addPostFrameCallback((_) {
58-
opController.show();
59-
});
54+
_userTextInputController.text =
55+
'Hey Gemini! Can you set the app color to purple?';
6056
}
6157

6258
@override
@@ -162,79 +158,34 @@ class _ChatDemoState extends ConsumerState<ChatDemo> {
162158
}
163159
}
164160

165-
void showModelPicker() {
166-
opController.hide();
167-
showDialog(
168-
context: context,
169-
builder: (context) {
170-
return ModelPicker(
171-
selectedModel: geminiModels.selectedModel,
172-
onSelected: (value) {
173-
_chatService.changeModel(value);
174-
setState(() {
175-
_userTextInputController.text =
176-
geminiModels.selectedModel.defaultPrompt;
177-
_messages.clear();
178-
});
179-
},
180-
);
181-
},
182-
);
183-
}
184-
185161
@override
186162
Widget build(BuildContext context) {
187163
return Scaffold(
188-
appBar: AppBar(
189-
backgroundColor: Colors.transparent,
190-
title: const Text('Chat Demo'),
191-
actions: [
192-
OverlayPortal(
193-
controller: opController,
194-
child: IconButton(
195-
onPressed: showModelPicker,
196-
icon: Icon(Icons.settings_outlined),
197-
),
198-
overlayChildBuilder: (context) {
199-
return Positioned(
200-
right: 0,
201-
top: 40,
202-
child: Dialog(
203-
insetAnimationDuration: Duration(milliseconds: 2000),
204-
constraints: BoxConstraints(maxWidth: 500),
205-
insetPadding: EdgeInsets.all(8),
206-
child: Padding(
207-
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
208-
child: Row(
209-
mainAxisSize: MainAxisSize.min,
210-
children: [Text('Try another model!')],
211-
),
164+
body: Column(
165+
children: [
166+
Expanded(
167+
child: AppFrame(
168+
child: Padding(
169+
padding: const EdgeInsets.all(AppSpacing.s16),
170+
child: Center(
171+
child: Column(
172+
mainAxisAlignment: MainAxisAlignment.center,
173+
children: <Widget>[
174+
Expanded(
175+
child: MessageListView(
176+
messages: _messages,
177+
scrollController: _scrollController,
178+
),
179+
),
180+
if (_loading) const LinearProgressIndicator(),
181+
AttachmentPreview(attachment: _attachment),
182+
],
212183
),
213184
),
214-
);
215-
},
216-
),
217-
],
218-
),
219-
body: AppFrame(
220-
child: Padding(
221-
padding: const EdgeInsets.all(AppSpacing.s16),
222-
child: Center(
223-
child: Column(
224-
mainAxisAlignment: MainAxisAlignment.center,
225-
children: <Widget>[
226-
Expanded(
227-
child: MessageListView(
228-
messages: _messages,
229-
scrollController: _scrollController,
230-
),
231-
),
232-
if (_loading) const LinearProgressIndicator(),
233-
AttachmentPreview(attachment: _attachment),
234-
],
185+
),
235186
),
236187
),
237-
),
188+
],
238189
),
239190
bottomNavigationBar: MessageInputBar(
240191
textController: _userTextInputController,

firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart

Lines changed: 0 additions & 9 deletions
This file was deleted.

firebase_ai_logic_showcase/lib/demos/chat/models/models.dart

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:typed_data';
16+
import 'dart:developer';
17+
import 'package:flutter/material.dart';
18+
import 'package:flutter/foundation.dart' show kIsWeb;
19+
import 'package:permission_handler/permission_handler.dart';
20+
import 'package:firebase_ai/firebase_ai.dart';
21+
import 'package:flutter_riverpod/flutter_riverpod.dart';
22+
import 'package:image_picker/image_picker.dart';
23+
import '../../shared/ui/app_frame.dart';
24+
import '../../shared/ui/app_spacing.dart';
25+
import '../../shared/ui/chat_components/ui_components.dart';
26+
import '../../shared/chat_service.dart';
27+
import '../../shared/ui/chat_components/model_picker.dart';
28+
import '../../shared/models/models.dart';
29+
30+
class ChatDemoNano extends ConsumerStatefulWidget {
31+
const ChatDemoNano({super.key, this.isSelected = false});
32+
final bool isSelected;
33+
34+
@override
35+
ConsumerState<ChatDemoNano> createState() => ChatDemoNanoState();
36+
}
37+
38+
class ChatDemoNanoState extends ConsumerState<ChatDemoNano> {
39+
// Service for interacting with the Gemini API.
40+
late final ChatService _chatService;
41+
42+
// UI State
43+
final List<MessageData> _messages = <MessageData>[];
44+
final TextEditingController _userTextInputController =
45+
TextEditingController();
46+
Uint8List? _attachment;
47+
final ScrollController _scrollController = ScrollController();
48+
bool _loading = false;
49+
OverlayPortalController opController = OverlayPortalController();
50+
static bool _pickerHasBeenShown = false;
51+
52+
@override
53+
void initState() {
54+
super.initState();
55+
_chatService = ChatService(ref);
56+
geminiModels.selectModel('gemini-2.5-flash-image-preview');
57+
_chatService.init();
58+
_userTextInputController.text =
59+
'Hot air balloons rising over the San Francisco Bay at golden hour with a view of the Golden Gate Bridge. Make it anime style.';
60+
_checkAndShowPicker();
61+
}
62+
63+
@override
64+
void didUpdateWidget(ChatDemoNano oldWidget) {
65+
super.didUpdateWidget(oldWidget);
66+
if (widget.isSelected != oldWidget.isSelected) {
67+
_checkAndShowPicker();
68+
}
69+
}
70+
71+
void _checkAndShowPicker() {
72+
if (widget.isSelected && !_pickerHasBeenShown) {
73+
_pickerHasBeenShown = true;
74+
WidgetsBinding.instance.addPostFrameCallback((_) {
75+
if (mounted) {
76+
showModelPicker();
77+
}
78+
});
79+
}
80+
}
81+
82+
@override
83+
void didChangeDependencies() {
84+
requestPermissions();
85+
super.didChangeDependencies();
86+
}
87+
88+
@override
89+
void dispose() {
90+
_scrollController.dispose();
91+
_userTextInputController.dispose();
92+
super.dispose();
93+
}
94+
95+
Future<void> requestPermissions() async {
96+
if (!kIsWeb) {
97+
await Permission.manageExternalStorage.request();
98+
}
99+
}
100+
101+
void _scrollToEnd() {
102+
WidgetsBinding.instance.addPostFrameCallback((_) {
103+
if (_scrollController.hasClients) {
104+
_scrollController.animateTo(
105+
_scrollController.position.maxScrollExtent,
106+
duration: const Duration(milliseconds: 300),
107+
curve: Curves.easeOut,
108+
);
109+
}
110+
});
111+
}
112+
113+
void _pickImage() async {
114+
final pickedImage = await ImagePicker().pickImage(
115+
source: ImageSource.gallery,
116+
);
117+
118+
if (pickedImage != null) {
119+
final imageBytes = await pickedImage.readAsBytes();
120+
setState(() {
121+
_attachment = imageBytes;
122+
});
123+
log('attachment saved!');
124+
}
125+
}
126+
127+
void sendMessage(String text) async {
128+
if (text.isEmpty) return;
129+
130+
setState(() {
131+
_loading = true;
132+
});
133+
134+
// Add user message to UI
135+
final userMessageText = text.trim();
136+
final userAttachment = _attachment;
137+
_messages.add(
138+
MessageData(
139+
text: userMessageText,
140+
image: userAttachment != null ? Image.memory(userAttachment) : null,
141+
fromUser: true,
142+
),
143+
);
144+
setState(() {
145+
_attachment = null;
146+
_userTextInputController.clear();
147+
});
148+
_scrollToEnd();
149+
150+
// Construct the Content object for the service
151+
final content = (userAttachment != null)
152+
? Content.multi([
153+
TextPart(userMessageText),
154+
InlineDataPart('image/jpeg', userAttachment),
155+
])
156+
: Content.text(userMessageText);
157+
158+
// Call the service and handle the response
159+
try {
160+
final chatResponse = await _chatService.sendMessage(content);
161+
_messages.add(
162+
MessageData(
163+
text: chatResponse.text,
164+
image: chatResponse.image,
165+
fromUser: false,
166+
),
167+
);
168+
} catch (e) {
169+
if (mounted) {
170+
ScaffoldMessenger.of(context).showSnackBar(
171+
SnackBar(
172+
content: Text(e.toString()),
173+
backgroundColor: Theme.of(context).colorScheme.error,
174+
),
175+
);
176+
}
177+
} finally {
178+
setState(() {
179+
_loading = false;
180+
});
181+
_scrollToEnd();
182+
}
183+
}
184+
185+
void showModelPicker() {
186+
showDialog(
187+
context: context,
188+
builder: (context) {
189+
return ModelPicker(
190+
selectedModel: geminiModels.selectedModel,
191+
onSelected: (value) {
192+
_chatService.changeModel(value);
193+
setState(() {
194+
_userTextInputController.text =
195+
geminiModels.selectedModel.defaultPrompt;
196+
_messages.clear();
197+
});
198+
},
199+
);
200+
},
201+
);
202+
}
203+
204+
@override
205+
Widget build(BuildContext context) {
206+
return Scaffold(
207+
body: Column(
208+
children: [
209+
Expanded(
210+
child: AppFrame(
211+
child: Padding(
212+
padding: const EdgeInsets.all(AppSpacing.s16),
213+
child: Center(
214+
child: Column(
215+
mainAxisAlignment: MainAxisAlignment.center,
216+
children: <Widget>[
217+
Expanded(
218+
child: MessageListView(
219+
messages: _messages,
220+
scrollController: _scrollController,
221+
),
222+
),
223+
if (_loading) const LinearProgressIndicator(),
224+
AttachmentPreview(attachment: _attachment),
225+
],
226+
),
227+
),
228+
),
229+
),
230+
),
231+
],
232+
),
233+
bottomNavigationBar: MessageInputBar(
234+
textController: _userTextInputController,
235+
loading: _loading,
236+
sendMessage: sendMessage,
237+
onPickImagePressed: _pickImage,
238+
),
239+
);
240+
}
241+
}

0 commit comments

Comments
 (0)