Skip to content
Merged
10 changes: 8 additions & 2 deletions packages/firebase_ai/firebase_ai/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ class _HomeScreenState extends State<HomeScreen> {
) {
switch (index) {
case 0:
return ChatPage(title: 'Chat', model: currentModel);
return ChatPage(
title: 'Chat',
useVertexBackend: useVertexBackend,
);
case 1:
return AudioPage(title: 'Audio', model: currentModel);
case 2:
Expand Down Expand Up @@ -199,7 +202,10 @@ class _HomeScreenState extends State<HomeScreen> {

default:
// Fallback to the first page in case of an unexpected index
return ChatPage(title: 'Chat', model: currentModel);
return ChatPage(
title: 'Chat',
useVertexBackend: useVertexBackend,
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class _AudioPageState extends State<AudioPage> {
Future<void> _submitAudioToModel(audioPart) async {
try {
String textPrompt = 'What is in the audio recording?';
final prompt = TextPart('What is in the audio recording?');
const prompt = TextPart('What is in the audio recording?');

setState(() {
_messages.add(MessageData(text: textPrompt, fromUser: true));
Expand Down Expand Up @@ -137,11 +137,6 @@ class _AudioPageState extends State<AudioPage> {
itemBuilder: (context, idx) {
return MessageWidget(
text: _messages[idx].text,
image: Image.memory(
_messages[idx].imageBytes!,
cacheWidth: 400,
cacheHeight: 400,
),
isFromUser: _messages[idx].fromUser ?? false,
);
},
Expand Down
64 changes: 54 additions & 10 deletions packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,58 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:firebase_ai/firebase_ai.dart';
import '../widgets/message_widget.dart';

class ChatPage extends StatefulWidget {
const ChatPage({super.key, required this.title, required this.model});
const ChatPage({
super.key,
required this.title,
required this.useVertexBackend,
});

final String title;
final GenerativeModel model;
final bool useVertexBackend;

@override
State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
ChatSession? _chat;
GenerativeModel? _model;
final ScrollController _scrollController = ScrollController();
final TextEditingController _textController = TextEditingController();
final FocusNode _textFieldFocus = FocusNode();
final List<MessageData> _messages = <MessageData>[];
bool _loading = false;
bool _enableThinking = false;

@override
void initState() {
super.initState();
_chat = widget.model.startChat();
_initializeChat();
}

void _initializeChat() {
final generationConfig = GenerationConfig(
thinkingConfig:
_enableThinking ? ThinkingConfig(includeThoughts: true) : null,
);
if (widget.useVertexBackend) {
_model = FirebaseAI.vertexAI(auth: FirebaseAuth.instance).generativeModel(
model: 'gemini-2.5-flash',
generationConfig: generationConfig,
);
} else {
_model = FirebaseAI.googleAI(auth: FirebaseAuth.instance).generativeModel(
model: 'gemini-2.5-flash',
generationConfig: generationConfig,
);
}
_chat = _model?.startChat();
}

void _scrollDown() {
Expand All @@ -64,18 +90,31 @@ class _ChatPageState extends State<ChatPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: const Text('Enable Thinking'),
value: _enableThinking,
onChanged: (bool value) {
setState(() {
_enableThinking = value;
_initializeChat();
});
},
),
Expanded(
child: ListView.builder(
controller: _scrollController,
itemBuilder: (context, idx) {
final message = _messages[idx];
return MessageWidget(
text: _messages[idx].text,
image: Image.memory(
_messages[idx].imageBytes!,
cacheWidth: 400,
cacheHeight: 400,
),
isFromUser: _messages[idx].fromUser ?? false,
text: message.text,
image: message.imageBytes != null
? Image.memory(
message.imageBytes!,
cacheWidth: 400,
cacheHeight: 400,
)
: null,
isFromUser: message.fromUser ?? false,
);
},
itemCount: _messages.length,
Expand Down Expand Up @@ -130,6 +169,11 @@ class _ChatPageState extends State<ChatPage> {
var response = await _chat?.sendMessage(
Content.text(message),
);
final thought = response?.thoughtSummary;
if (thought != null) {
_messages
.add(MessageData(text: thought, fromUser: false, isThought: true));
}
var text = response?.text;
_messages.add(MessageData(text: text, fromUser: false));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class _DocumentPageState extends State<DocumentPage> {
const _prompt =
'Write me a summary in one sentence what this document is about.';

final prompt = TextPart(_prompt);
const prompt = TextPart(_prompt);

setState(() {
_messages.add(MessageData(text: _prompt, fromUser: true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,39 @@ class Location {
}

class _FunctionCallingPageState extends State<FunctionCallingPage> {
late final GenerativeModel _functionCallModel;
late GenerativeModel _functionCallModel;
final List<MessageData> _messages = <MessageData>[];
bool _loading = false;
bool _enableThinking = false;

@override
void initState() {
super.initState();
_initializeModel();
}

void _initializeModel() {
final generationConfig = GenerationConfig(
thinkingConfig:
_enableThinking ? ThinkingConfig(includeThoughts: true) : null,
);
if (widget.useVertexBackend) {
var vertexAI = FirebaseAI.vertexAI(auth: FirebaseAuth.instance);
_functionCallModel = vertexAI.generativeModel(
model: 'gemini-2.0-flash',
model: 'gemini-2.5-flash',
tools: [
Tool.functionDeclarations([fetchWeatherTool]),
],
generationConfig: generationConfig,
);
} else {
var googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
_functionCallModel = googleAI.generativeModel(
model: 'gemini-2.0-flash',
model: 'gemini-2.5-flash',
tools: [
Tool.functionDeclarations([fetchWeatherTool]),
],
generationConfig: generationConfig,
);
}
}
Expand Down Expand Up @@ -118,12 +129,24 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: const Text('Enable Thinking'),
value: _enableThinking,
onChanged: (bool value) {
setState(() {
_enableThinking = value;
_initializeModel();
});
},
),
Expanded(
child: ListView.builder(
itemBuilder: (context, idx) {
final message = _messages[idx];
return MessageWidget(
text: _messages[idx].text,
isFromUser: _messages[idx].fromUser ?? false,
text: message.text,
isFromUser: message.fromUser ?? false,
isThought: message.isThought,
);
},
itemCount: _messages.length,
Expand Down Expand Up @@ -158,43 +181,87 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
Future<void> _testFunctionCalling() async {
setState(() {
_loading = true;
_messages.clear();
});
final functionCallChat = _functionCallModel.startChat();
const prompt = 'What is the weather like in Boston on 10/02 in year 2024?';
try {
final functionCallChat = _functionCallModel.startChat();
const prompt =
'What is the weather like in Boston on 10/02 in year 2024?';

// Send the message to the generative model.
var response = await functionCallChat.sendMessage(
Content.text(prompt),
);
_messages.add(MessageData(text: prompt, fromUser: true));

final functionCalls = response.functionCalls.toList();
// When the model response with a function call, invoke the function.
if (functionCalls.isNotEmpty) {
final functionCall = functionCalls.first;
if (functionCall.name == 'fetchWeather') {
Map<String, dynamic> location =
functionCall.args['location']! as Map<String, dynamic>;
var date = functionCall.args['date']! as String;
var city = location['city'] as String;
var state = location['state'] as String;
final functionResult = await fetchWeather(Location(city, state), date);
// Send the response to the model so that it can use the result to
// generate text for the user.
response = await functionCallChat.sendMessage(
Content.functionResponse(functionCall.name, functionResult),
);
} else {
throw UnimplementedError(
'Function not declared to the model: ${functionCall.name}',
);
// Send the message to the generative model.
var response = await functionCallChat.sendMessage(
Content.text(prompt),
);

final thought = response.thoughtSummary;
if (thought != null) {
_messages
.add(MessageData(text: thought, fromUser: false, isThought: true));
}
}
// When the model responds with non-null text content, print it.
if (response.text case final text?) {
_messages.add(MessageData(text: text));

final functionCalls = response.functionCalls.toList();
// When the model response with a function call, invoke the function.
if (functionCalls.isNotEmpty) {
final functionCall = functionCalls.first;
if (functionCall.name == 'fetchWeather') {
Map<String, dynamic> location =
functionCall.args['location']! as Map<String, dynamic>;
var date = functionCall.args['date']! as String;
var city = location['city'] as String;
var state = location['state'] as String;
final functionResult =
await fetchWeather(Location(city, state), date);
// Send the response to the model so that it can use the result to
// generate text for the user.
response = await functionCallChat.sendMessage(
Content.functionResponse(functionCall.name, functionResult),
);
} else {
throw UnimplementedError(
'Function not declared to the model: ${functionCall.name}',
);
}
}
// When the model responds with non-null text content, print it.
if (response.text case final text?) {
_messages.add(MessageData(text: text));
setState(() {
_loading = false;
});
}
} catch (e) {
_showError(e.toString());
setState(() {
_loading = false;
});
} finally {
setState(() {
_loading = false;
});
}
}

void _showError(String message) {
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Something went wrong'),
content: SingleChildScrollView(
child: SelectableText(message),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class _ImagePromptPageState extends State<ImagePromptPage> {
final content = [
Content.multi([
TextPart(message),
FileData(
const FileData(
'image/jpeg',
'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg',
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ class _VideoPageState extends State<VideoPage> {

const _prompt = 'Can you tell me what is in the video?';

final prompt = TextPart(_prompt);

setState(() {
_messages.add(MessageData(text: _prompt, fromUser: true));
});
Expand All @@ -56,7 +54,7 @@ class _VideoPageState extends State<VideoPage> {
InlineDataPart('video/mp4', videoBytes.buffer.asUint8List());

final response = await widget.model.generateContent([
Content.multi([prompt, videoPart]),
Content.multi([const TextPart(_prompt), videoPart]),
]);

setState(() {
Expand Down
Loading
Loading