diff --git a/.gitignore b/.gitignore index c45e8eaf..febe08b9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.vscode/ # IntelliJ related *.iml diff --git a/vertexai/.gitignore b/vertexai/.gitignore index d61c3825..cac915e7 100644 --- a/vertexai/.gitignore +++ b/vertexai/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/vertexai/assets/documents/gemini_summary.pdf b/vertexai/assets/documents/gemini_summary.pdf new file mode 100644 index 00000000..08881c78 Binary files /dev/null and b/vertexai/assets/documents/gemini_summary.pdf differ diff --git a/vertexai/assets/videos/landscape.mp4 b/vertexai/assets/videos/landscape.mp4 new file mode 100644 index 00000000..a7f4298d Binary files /dev/null and b/vertexai/assets/videos/landscape.mp4 differ diff --git a/vertexai/lib/main.dart b/vertexai/lib/main.dart index 68f6eec2..d4c365b8 100644 --- a/vertexai/lib/main.dart +++ b/vertexai/lib/main.dart @@ -12,18 +12,41 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:firebase_vertexai/firebase_vertexai.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import 'package:flutter/material.dart'; -void main() { - runApp(const GenerativeAISample()); +import 'pages/chat_page.dart'; +import 'pages/audio_page.dart'; +import 'pages/function_calling_page.dart'; +import 'pages/image_prompt_page.dart'; +import 'pages/token_count_page.dart'; +import 'pages/schema_page.dart'; +import 'pages/imagen_page.dart'; +import 'pages/document.dart'; +import 'pages/video_page.dart'; +import 'pages/bidi_page.dart'; + +// REQUIRED if you want to run on Web +const FirebaseOptions? options = null; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + // await FirebaseAuth.instance.signInAnonymously(); + + var vertexInstance = + FirebaseVertexAI.instanceFor(auth: FirebaseAuth.instance); + final model = vertexInstance.generativeModel(model: 'gemini-1.5-flash'); + + runApp(GenerativeAISample(model: model)); } class GenerativeAISample extends StatelessWidget { - const GenerativeAISample({super.key}); + final GenerativeModel model; + + const GenerativeAISample({super.key, required this.model}); @override Widget build(BuildContext context) { @@ -36,436 +59,139 @@ class GenerativeAISample extends StatelessWidget { ), useMaterial3: true, ), - home: const ChatScreen(title: 'Flutter + Vertex AI'), - ); - } -} - -class ChatScreen extends StatefulWidget { - const ChatScreen({super.key, required this.title}); - - final String title; - - @override - State createState() => _ChatScreenState(); -} - -class _ChatScreenState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: const ChatWidget(), + home: HomeScreen(model: model), ); } } -class ChatWidget extends StatefulWidget { - const ChatWidget({ - super.key, - }); +class HomeScreen extends StatefulWidget { + final GenerativeModel model; + const HomeScreen({super.key, required this.model}); @override - State createState() => _ChatWidgetState(); + State createState() => _HomeScreenState(); } -class _ChatWidgetState extends State { - late final GenerativeModel _model; - late final GenerativeModel _functionCallModel; - late final ChatSession _chat; - final ScrollController _scrollController = ScrollController(); - final TextEditingController _textController = TextEditingController(); - final FocusNode _textFieldFocus = FocusNode(); - final List<({Image? image, String? text, bool fromUser})> _generatedContent = - <({Image? image, String? text, bool fromUser})>[]; - bool _loading = false; - - @override - void initState() { - super.initState(); +class _HomeScreenState extends State { + int _selectedIndex = 0; + + List get _pages => [ + // Build _pages dynamically + ChatPage(title: 'Chat', model: widget.model), + AudioPage(title: 'Audio', model: widget.model), + TokenCountPage(title: 'Token Count', model: widget.model), + const FunctionCallingPage( + title: 'Function Calling', + ), // function calling will initial its own model + ImagePromptPage(title: 'Image Prompt', model: widget.model), + ImagenPage(title: 'Imagen Model', model: widget.model), + SchemaPromptPage(title: 'Schema Prompt', model: widget.model), + DocumentPage(title: 'Document Prompt', model: widget.model), + VideoPage(title: 'Video Prompt', model: widget.model), + BidiPage(title: 'Bidi Stream', model: widget.model), + ]; - initFirebase().then((value) { - _model = FirebaseVertexAI.instance.generativeModel( - model: 'gemini-2.0-flash', - ); - _functionCallModel = FirebaseVertexAI.instance.generativeModel( - model: 'gemini-2.0-flash', - tools: [ - Tool(functionDeclarations: [ - FunctionDeclaration( - 'fetchCurrentWeather', - 'Returns the weather in a given location.', - Schema(SchemaType.object, properties: { - 'location': Schema(SchemaType.string, - description: 'A location name, like "London".'), - }, requiredProperties: [ - 'location' - ])) - ]) - ], - ); - _chat = _model.startChat(); + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; }); } - Future initFirebase() async { - await Firebase.initializeApp(); - } - - void _scrollDown() { - WidgetsBinding.instance.addPostFrameCallback( - (_) => _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration( - milliseconds: 750, - ), - curve: Curves.easeOutCirc, - ), - ); - } - @override Widget build(BuildContext context) { - final 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, - ), + return Scaffold( + appBar: AppBar( + title: const Text('Flutter + Vertex AI'), ), - focusedBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all( - Radius.circular(14), - ), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.secondary, - ), + body: Center( + child: _pages.elementAt(_selectedIndex), ), - ); - - return Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemBuilder: (context, idx) { - var content = _generatedContent[idx]; - return MessageWidget( - text: content.text, - image: content.image, - isFromUser: content.fromUser, - ); - }, - itemCount: _generatedContent.length, + bottomNavigationBar: BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: Icon( + Icons.chat, + color: Theme.of(context).colorScheme.primary, ), + label: 'Chat', + tooltip: 'Chat', ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 25, - horizontal: 15, + BottomNavigationBarItem( + icon: Icon( + Icons.mic, + color: Theme.of(context).colorScheme.primary, ), - child: Row( - children: [ - Expanded( - child: TextField( - autofocus: true, - focusNode: _textFieldFocus, - decoration: textFieldDecoration, - controller: _textController, - onSubmitted: _sendChatMessage, - ), - ), - const SizedBox.square( - dimension: 15, - ), - IconButton( - tooltip: 'tokenCount Test', - onPressed: !_loading - ? () async { - await _testCountToken(); - } - : null, - icon: Icon( - Icons.numbers, - color: _loading - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, - ), - ), - IconButton( - tooltip: 'image prompt', - onPressed: !_loading - ? () async { - await _sendImagePrompt(_textController.text); - } - : null, - icon: Icon( - Icons.image, - color: _loading - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, - ), - ), - IconButton( - tooltip: 'storage prompt', - onPressed: !_loading - ? () async { - await _sendStorageUriPrompt(_textController.text); - } - : null, - icon: Icon( - Icons.folder, - color: _loading - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, - ), - ), - if (!_loading) - IconButton( - onPressed: () async { - await _sendChatMessage(_textController.text); - }, - icon: Icon( - Icons.send, - color: Theme.of(context).colorScheme.primary, - ), - ) - else - const CircularProgressIndicator(), - ], + label: 'Audio Prompt', + tooltip: 'Audio Prompt', + ), + BottomNavigationBarItem( + icon: Icon( + Icons.numbers, + color: Theme.of(context).colorScheme.primary, ), + label: 'Token Count', + tooltip: 'Token Count', ), - ], - ), - ); - } - - Future _sendStorageUriPrompt(String message) async { - setState(() { - _loading = true; - }); - try { - final content = [ - Content.multi([ - TextPart(message), - FileData( - 'image/jpeg', - 'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg', + BottomNavigationBarItem( + icon: Icon( + Icons.functions, + color: Theme.of(context).colorScheme.primary, + ), + label: 'Function Calling', + tooltip: 'Function Calling', ), - ]), - ]; - _generatedContent.add((image: null, text: message, fromUser: true)); - - var response = await _model.generateContent(content); - var text = response.text; - _generatedContent.add((image: null, text: text, fromUser: false)); - - if (text == null) { - _showError('No response from API.'); - return; - } else { - setState(() { - _loading = false; - _scrollDown(); - }); - } - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - _textController.clear(); - setState(() { - _loading = false; - }); - _textFieldFocus.requestFocus(); - } - } - - Future _sendImagePrompt(String message) async { - setState(() { - _loading = true; - }); - try { - ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); - ByteData sconeBytes = await rootBundle.load('assets/images/scones.jpg'); - final content = [ - Content.multi([ - TextPart(message), - // The only accepted mime types are image/*. - DataPart('image/jpeg', catBytes.buffer.asUint8List()), - DataPart('image/jpeg', sconeBytes.buffer.asUint8List()), - ]), - ]; - _generatedContent.add( - ( - image: Image.asset('assets/images/cat.jpg'), - text: message, - fromUser: true - ), - ); - _generatedContent.add( - ( - image: Image.asset('assets/images/scones.jpg'), - text: null, - fromUser: true - ), - ); - - var response = await _model.generateContent(content); - var text = response.text; - _generatedContent.add((image: null, text: text, fromUser: false)); - - if (text == null) { - _showError('No response from API.'); - return; - } else { - setState(() { - _loading = false; - _scrollDown(); - }); - } - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - _textController.clear(); - setState(() { - _loading = false; - }); - _textFieldFocus.requestFocus(); - } - } - - Future _sendChatMessage(String message) async { - setState(() { - _loading = true; - }); - - try { - _generatedContent.add((image: null, text: message, fromUser: true)); - var response = await _chat.sendMessage( - Content.text(message), - ); - var text = response.text; - _generatedContent.add((image: null, text: text, fromUser: false)); - - if (text == null) { - _showError('No response from API.'); - return; - } else { - setState(() { - _loading = false; - _scrollDown(); - }); - } - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - _textController.clear(); - setState(() { - _loading = false; - }); - _textFieldFocus.requestFocus(); - } - } - - Future _testCountToken() async { - setState(() { - _loading = true; - }); - - const prompt = 'tell a short story'; - var response = await _model.countTokens([Content.text(prompt)]); - print( - 'token: ${response.totalTokens}, billable characters: ${response.totalBillableCharacters}', - ); - - setState(() { - _loading = false; - }); - } - - void _showError(String message) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Something went wrong'), - content: SingleChildScrollView( - child: SelectableText(message), + BottomNavigationBarItem( + icon: Icon( + Icons.image, + color: Theme.of(context).colorScheme.primary, + ), + label: 'Image Prompt', + tooltip: 'Image Prompt', ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('OK'), + BottomNavigationBarItem( + icon: Icon( + Icons.image_search, + color: Theme.of(context).colorScheme.primary, ), - ], - ); - }, - ); - } -} - -class MessageWidget extends StatelessWidget { - final Image? image; - final String? text; - final bool isFromUser; - - const MessageWidget({ - super.key, - this.image, - this.text, - required this.isFromUser, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: - isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start, - children: [ - Flexible( - child: Container( - constraints: const BoxConstraints(maxWidth: 600), - decoration: BoxDecoration( - color: isFromUser - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(18), + label: 'Imagen Model', + tooltip: 'Imagen Model', + ), + BottomNavigationBarItem( + icon: Icon( + Icons.schema, + color: Theme.of(context).colorScheme.primary, ), - padding: const EdgeInsets.symmetric( - vertical: 15, - horizontal: 20, + label: 'Schema Prompt', + tooltip: 'Schema Prompt', + ), + BottomNavigationBarItem( + icon: Icon( + Icons.edit_document, + color: Theme.of(context).colorScheme.primary, ), - margin: const EdgeInsets.only(bottom: 8), - child: Column( - children: [ - if (text case final text?) MarkdownBody(data: text), - if (image case final image?) image, - ], + label: 'Document Prompt', + tooltip: 'Document Prompt', + ), + BottomNavigationBarItem( + icon: Icon( + Icons.video_collection, + color: Theme.of(context).colorScheme.primary, ), + label: 'Video Prompt', + tooltip: 'Video Prompt', ), - ), - ], + BottomNavigationBarItem( + icon: Icon( + Icons.stream, + color: Theme.of(context).colorScheme.primary, + ), + label: 'Bidi Stream', + tooltip: 'Bidi Stream', + ), + ], + currentIndex: _selectedIndex, + onTap: _onItemTapped, + ), ); } } diff --git a/vertexai/lib/pages/audio_page.dart b/vertexai/lib/pages/audio_page.dart new file mode 100644 index 00000000..63862e18 --- /dev/null +++ b/vertexai/lib/pages/audio_page.dart @@ -0,0 +1,186 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import '../widgets/message_widget.dart'; +import 'package:record/record.dart'; +import 'package:path_provider/path_provider.dart'; + +final record = AudioRecorder(); + +class AudioPage extends StatefulWidget { + const AudioPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _AudioPageState(); +} + +class _AudioPageState extends State { + ChatSession? chat; + final ScrollController _scrollController = ScrollController(); + final List _messages = []; + bool _recording = false; + + @override + void initState() { + super.initState(); + chat = widget.model.startChat(); + } + + Future recordAudio() async { + if (!await record.hasPermission()) { + print('Audio recording permission denied'); + return; + } + + final dir = Directory( + '${(await getApplicationDocumentsDirectory()).path}/libs/recordings', + ); + + // ignore: avoid_slow_async_io + if (!await dir.exists()) { + await dir.create(recursive: true); + } + + String filePath = + '${dir.path}/recording_${DateTime.now().millisecondsSinceEpoch}.wav'; + + await record.start( + const RecordConfig( + encoder: AudioEncoder.wav, + ), + path: filePath, + ); + } + + Future stopRecord() async { + var path = await record.stop(); + + if (path == null) { + print('Failed to stop recording'); + return; + } + + debugPrint('Recording saved to: $path'); + + try { + File file = File(path); + final audio = await file.readAsBytes(); + debugPrint('Audio file size: ${audio.length} bytes'); + + final audioPart = InlineDataPart('audio/wav', audio); + + await _submitAudioToModel(audioPart); + + await file.delete(); + debugPrint('Recording deleted successfully.'); + } catch (e) { + debugPrint('Error processing recording: $e'); + } + } + + Future _submitAudioToModel(audioPart) async { + try { + String textPrompt = 'What is in the audio recording?'; + final prompt = TextPart('What is in the audio recording?'); + + setState(() { + _messages.add(MessageData(text: textPrompt, fromUser: true)); + }); + + final response = await widget.model.generateContent([ + Content.multi([prompt, audioPart]), + ]); + + setState(() { + _messages.add(MessageData(text: response.text, fromUser: false)); + }); + + debugPrint(response.text); + } catch (e) { + debugPrint('Error sending audio to model: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + image: _messages[idx].image, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + IconButton( + onPressed: () async { + setState(() { + _recording = !_recording; + }); + if (_recording) { + await recordAudio(); + } else { + await stopRecord(); + } + }, + icon: Icon( + Icons.mic, + color: _recording + ? Colors.blueGrey + : Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox.square( + dimension: 15, + ), + const Text( + 'Tap the mic to record, tap again to submit', + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/vertexai/lib/pages/bidi_page.dart b/vertexai/lib/pages/bidi_page.dart new file mode 100644 index 00000000..0192c023 --- /dev/null +++ b/vertexai/lib/pages/bidi_page.dart @@ -0,0 +1,452 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import 'dart:typed_data'; +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import '../widgets/message_widget.dart'; +import '../utils/audio_player.dart'; +import '../utils/audio_recorder.dart'; + +class BidiPage extends StatefulWidget { + const BidiPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _BidiPageState(); +} + +class LightControl { + final int? brightness; + final String? colorTemperature; + + LightControl({this.brightness, this.colorTemperature}); +} + +class _BidiPageState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(); + final List _messages = []; + bool _loading = false; + bool _sessionOpening = false; + bool _recording = false; + late LiveGenerativeModel _liveModel; + late LiveSession _session; + final _audioManager = AudioStreamManager(); + final _audioRecorder = InMemoryAudioRecorder(); + var _chunkBuilder = BytesBuilder(); + var _audioIndex = 0; + StreamController _stopController = StreamController(); + + @override + void initState() { + super.initState(); + + final config = LiveGenerationConfig( + speechConfig: SpeechConfig(voice: Voice.fenrir), + responseModalities: [ + ResponseModalities.audio, + ], + ); + + _liveModel = FirebaseVertexAI.instance.liveGenerativeModel( + model: 'gemini-2.0-flash-exp', + liveGenerationConfig: config, + tools: [ + Tool.functionDeclarations([lightControlTool]), + ], + ); + } + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + void dispose() { + if (_sessionOpening) { + _audioManager.stopAudioPlayer(); + _audioManager.disposeAudioPlayer(); + + _audioRecorder.stopRecording(); + + _stopController.close(); + + _sessionOpening = false; + _session.close(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + image: _messages[idx].image, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + controller: _textController, + onSubmitted: _sendTextPrompt, + ), + ), + const SizedBox.square( + dimension: 15, + ), + IconButton( + tooltip: 'Start Streaming', + onPressed: !_loading + ? () async { + await _setupSession(); + } + : null, + icon: Icon( + Icons.network_wifi, + color: _sessionOpening + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary, + ), + ), + IconButton( + tooltip: 'Send Stream Message', + onPressed: !_loading + ? () async { + if (_recording) { + await _stopRecording(); + } else { + await _startRecording(); + } + } + : null, + icon: Icon( + _recording ? Icons.stop : Icons.mic, + color: _loading + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary, + ), + ), + if (!_loading) + IconButton( + onPressed: () async { + await _sendTextPrompt(_textController.text); + }, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ), + ); + } + + final lightControlTool = FunctionDeclaration( + 'setLightValues', + 'Set the brightness and color temperature of a room light.', + parameters: { + 'brightness': Schema.integer( + description: 'Light level from 0 to 100. ' + 'Zero is off and 100 is full brightness.', + ), + 'colorTemperature': Schema.string( + description: 'Color temperature of the light fixture, ' + 'which can be `daylight`, `cool` or `warm`.', + ), + }, + ); + + Future> _setLightValues({ + int? brightness, + String? colorTemperature, + }) async { + final apiResponse = { + 'colorTemprature': 'warm', + 'brightness': brightness, + }; + return apiResponse; + } + + Future _setupSession() async { + setState(() { + _loading = true; + }); + + if (!_sessionOpening) { + _session = await _liveModel.connect(); + _sessionOpening = true; + _stopController = StreamController(); + unawaited( + processMessagesContinuously( + stopSignal: _stopController, + ), + ); + } else { + _stopController.add(true); + await _stopController.close(); + + await _session.close(); + await _audioManager.stopAudioPlayer(); + await _audioManager.disposeAudioPlayer(); + _sessionOpening = false; + } + + setState(() { + _loading = false; + }); + } + + Future _startRecording() async { + setState(() { + _recording = true; + }); + try { + await _audioRecorder.checkPermission(); + final audioRecordStream = _audioRecorder.startRecordingStream(); + // Map the Uint8List stream to InlineDataPart stream + final mediaChunkStream = audioRecordStream.map((data) { + return InlineDataPart('audio/pcm', data); + }); + await _session.sendMediaStream(mediaChunkStream); + } catch (e) { + _showError(e.toString()); + } + } + + Future _stopRecording() async { + try { + await _audioRecorder.stopRecording(); + } catch (e) { + _showError(e.toString()); + } + + setState(() { + _recording = false; + }); + } + + Future _sendTextPrompt(String textPrompt) async { + setState(() { + _loading = true; + }); + try { + final prompt = Content.text(textPrompt); + await _session.send(input: prompt, turnComplete: true); + } catch (e) { + _showError(e.toString()); + } + + setState(() { + _loading = false; + }); + } + + Future processMessagesContinuously({ + required StreamController stopSignal, + }) async { + bool shouldContinue = true; + + //listen to the stop signal stream + stopSignal.stream.listen((stop) { + if (stop) { + shouldContinue = false; + } + }); + + while (shouldContinue) { + try { + await for (final message in _session.receive()) { + // Process the received message + await _handleLiveServerMessage(message); + } + } catch (e) { + _showError(e.toString()); + break; + } + + // Optionally add a delay before restarting, if needed + await Future.delayed( + const Duration(milliseconds: 100), + ); // Small delay to prevent tight loops + } + } + + Future _handleLiveServerMessage(LiveServerMessage response) async { + if (response is LiveServerContent && response.modelTurn != null) { + await _handleLiveServerContent(response); + } + + if (response is LiveServerContent && + response.turnComplete != null && + response.turnComplete!) { + await _handleTurnComplete(); + } + + if (response is LiveServerContent && + response.interrupted != null && + response.interrupted!) { + log('Interrupted: $response'); + } + + if (response is LiveServerToolCall && response.functionCalls != null) { + await _handleLiveServerToolCall(response); + } + } + + Future _handleLiveServerContent(LiveServerContent response) async { + final partList = response.modelTurn?.parts; + if (partList != null) { + for (final part in partList) { + if (part is TextPart) { + await _handleTextPart(part); + } else if (part is InlineDataPart) { + await _handleInlineDataPart(part); + } else { + log('receive part with type ${part.runtimeType}'); + } + } + } + } + + Future _handleTextPart(TextPart part) async { + if (!_loading) { + setState(() { + _loading = true; + }); + } + _messages.add(MessageData(text: part.text, fromUser: false)); + setState(() { + _loading = false; + _scrollDown(); + }); + } + + Future _handleInlineDataPart(InlineDataPart part) async { + if (part.mimeType.startsWith('audio')) { + _chunkBuilder.add(part.bytes); + _audioIndex++; + if (_audioIndex == 15) { + Uint8List chunk = await audioChunkWithHeader( + _chunkBuilder.toBytes(), + 24000, + ); + _audioManager.addAudio(chunk); + _chunkBuilder.clear(); + _audioIndex = 0; + } + } + } + + Future _handleTurnComplete() async { + if (_chunkBuilder.isNotEmpty) { + Uint8List chunk = await audioChunkWithHeader( + _chunkBuilder.toBytes(), + 24000, + ); + _audioManager.addAudio(chunk); + _audioIndex = 0; + _chunkBuilder.clear(); + } + } + + Future _handleLiveServerToolCall(LiveServerToolCall response) async { + final functionCalls = response.functionCalls!.toList(); + if (functionCalls.isNotEmpty) { + final functionCall = functionCalls.first; + if (functionCall.name == 'setLightValues') { + var color = functionCall.args['colorTemperature']! as String; + var brightness = functionCall.args['brightness']! as int; + final functionResult = await _setLightValues( + brightness: brightness, + colorTemperature: color, + ); + await _session.send( + input: Content.functionResponse(functionCall.name, functionResult), + ); + } else { + throw UnimplementedError( + 'Function not declared to the model: ${functionCall.name}', + ); + } + } + } + + void _showError(String message) { + showDialog( + 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'), + ), + ], + ); + }, + ); + } +} diff --git a/vertexai/lib/pages/chat_page.dart b/vertexai/lib/pages/chat_page.dart new file mode 100644 index 00000000..33b6e114 --- /dev/null +++ b/vertexai/lib/pages/chat_page.dart @@ -0,0 +1,176 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import '../widgets/message_widget.dart'; + +class ChatPage extends StatefulWidget { + const ChatPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _ChatPageState(); +} + +class _ChatPageState extends State { + ChatSession? _chat; + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(); + final List _messages = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + _chat = widget.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) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + image: _messages[idx].image, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + controller: _textController, + onSubmitted: _sendChatMessage, + ), + ), + const SizedBox.square( + dimension: 15, + ), + if (!_loading) + IconButton( + onPressed: () async { + await _sendChatMessage(_textController.text); + }, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ), + ); + } + + Future _sendChatMessage(String message) async { + setState(() { + _loading = true; + }); + + try { + _messages.add(MessageData(text: message, fromUser: true)); + var response = await _chat?.sendMessage( + Content.text(message), + ); + var text = response?.text; + _messages.add(MessageData(text: text, fromUser: false)); + + if (text == null) { + _showError('No response from API.'); + 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( + 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'), + ), + ], + ); + }, + ); + } +} diff --git a/vertexai/lib/pages/document.dart b/vertexai/lib/pages/document.dart new file mode 100644 index 00000000..ff98680f --- /dev/null +++ b/vertexai/lib/pages/document.dart @@ -0,0 +1,117 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import 'package:flutter/services.dart'; +import '../widgets/message_widget.dart'; + +class DocumentPage extends StatefulWidget { + const DocumentPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _DocumentPageState(); +} + +class _DocumentPageState extends State { + ChatSession? chat; + late final GenerativeModel model; + final List _messages = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + chat = widget.model.startChat(); + } + + Future _testDocumentReading(model) async { + try { + ByteData docBytes = + await rootBundle.load('assets/documents/gemini_summary.pdf'); + + const _prompt = + 'Write me a summary in one sentence what this document is about.'; + + final prompt = TextPart(_prompt); + + setState(() { + _messages.add(MessageData(text: _prompt, fromUser: true)); + }); + + final pdfPart = + InlineDataPart('application/pdf', docBytes.buffer.asUint8List()); + + final response = await widget.model.generateContent([ + Content.multi([prompt, pdfPart]), + ]); + + setState(() { + _messages.add(MessageData(text: response.text, fromUser: false)); + }); + } catch (e) { + print('Error sending document to model: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Center( + child: SizedBox( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testDocumentReading(widget.model); + } + : null, + child: const Text('Test Document Reading'), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/vertexai/lib/pages/function_calling_page.dart b/vertexai/lib/pages/function_calling_page.dart new file mode 100644 index 00000000..130afff5 --- /dev/null +++ b/vertexai/lib/pages/function_calling_page.dart @@ -0,0 +1,186 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../widgets/message_widget.dart'; + +class FunctionCallingPage extends StatefulWidget { + const FunctionCallingPage({super.key, required this.title}); + + final String title; + + @override + State createState() => _FunctionCallingPageState(); +} + +class Location { + final String city; + final String state; + + Location(this.city, this.state); +} + +class _FunctionCallingPageState extends State { + late final GenerativeModel _functionCallModel; + final List _messages = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + var vertex_instance = + FirebaseVertexAI.instanceFor(auth: FirebaseAuth.instance); + _functionCallModel = vertex_instance.generativeModel( + model: 'gemini-1.5-flash', + tools: [ + Tool.functionDeclarations([fetchWeatherTool]), + ], + ); + } + + // This is a hypothetical API to return a fake weather data collection for + // certain location + Future> fetchWeather( + Location location, + String date, + ) async { + // TODO(developer): Call a real weather API. + // Mock response from the API. In developer live code this would call the + // external API and return what that API returns. + final apiResponse = { + 'temperature': 38, + 'chancePrecipitation': '56%', + 'cloudConditions': 'partly-cloudy', + }; + return apiResponse; + } + + /// Actual function to demonstrate the function calling feature. + final fetchWeatherTool = FunctionDeclaration( + 'fetchWeather', + 'Get the weather conditions for a specific city on a specific date.', + parameters: { + 'location': Schema.object( + description: 'The name of the city and its state for which to get ' + 'the weather. Only cities in the USA are supported.', + properties: { + 'city': Schema.string( + description: 'The city of the location.', + ), + 'state': Schema.string( + description: 'The state of the location.', + ), + }, + ), + 'date': Schema.string( + description: 'The date for which to get the weather. ' + 'Date must be in the format: YYYY-MM-DD.', + ), + }, + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testFunctionCalling(); + } + : null, + child: const Text('Test Function Calling'), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _testFunctionCalling() async { + setState(() { + _loading = true; + }); + final functionCallChat = _functionCallModel.startChat(); + const prompt = 'What is the weather like in Boston on 10/02 this year?'; + + // Send the message to the generative model. + var response = await functionCallChat.sendMessage( + Content.text(prompt), + ); + + 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 location = + functionCall.args['location']! as Map; + 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; + }); + } + } +} diff --git a/vertexai/lib/pages/image_prompt_page.dart b/vertexai/lib/pages/image_prompt_page.dart new file mode 100644 index 00000000..0d84c594 --- /dev/null +++ b/vertexai/lib/pages/image_prompt_page.dart @@ -0,0 +1,243 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import 'package:flutter/services.dart'; +import '../widgets/message_widget.dart'; + +class ImagePromptPage extends StatefulWidget { + const ImagePromptPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _ImagePromptPageState(); +} + +class _ImagePromptPageState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(); + final List _generatedContent = []; + bool _loading = false; + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + var content = _generatedContent[idx]; + return MessageWidget( + text: content.text, + image: content.image, + isFromUser: content.fromUser ?? false, + ); + }, + itemCount: _generatedContent.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + controller: _textController, + ), + ), + const SizedBox.square( + dimension: 15, + ), + if (!_loading) + IconButton( + onPressed: () async { + await _sendImagePrompt(_textController.text); + }, + icon: Icon( + Icons.image, + color: Theme.of(context).colorScheme.primary, + ), + ), + if (!_loading) + IconButton( + onPressed: () async { + await _sendStorageUriPrompt(_textController.text); + }, + icon: Icon( + Icons.storage, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ), + ); + } + + Future _sendImagePrompt(String message) async { + setState(() { + _loading = true; + }); + try { + ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); + ByteData sconeBytes = await rootBundle.load('assets/images/scones.jpg'); + final content = [ + Content.multi([ + TextPart(message), + // The only accepted mime types are image/*. + InlineDataPart('image/jpeg', catBytes.buffer.asUint8List()), + InlineDataPart('image/jpeg', sconeBytes.buffer.asUint8List()), + ]), + ]; + _generatedContent.add( + MessageData( + image: Image.asset('assets/images/cat.jpg'), + text: message, + fromUser: true, + ), + ); + _generatedContent.add( + MessageData( + image: Image.asset('assets/images/scones.jpg'), + fromUser: true, + ), + ); + + var response = await widget.model.generateContent(content); + var text = response.text; + _generatedContent.add(MessageData(text: text, fromUser: false)); + + if (text == null) { + _showError('No response from API.'); + return; + } else { + setState(() { + _loading = false; + _scrollDown(); + }); + } + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + Future _sendStorageUriPrompt(String message) async { + setState(() { + _loading = true; + }); + try { + final content = [ + Content.multi([ + TextPart(message), + FileData( + 'image/jpeg', + 'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg', + ), + ]), + ]; + _generatedContent.add(MessageData(text: message, fromUser: true)); + + var response = await widget.model.generateContent(content); + var text = response.text; + _generatedContent.add(MessageData(text: text, fromUser: false)); + + if (text == null) { + _showError('No response from API.'); + 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( + 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'), + ), + ], + ); + }, + ); + } +} diff --git a/vertexai/lib/pages/imagen_page.dart b/vertexai/lib/pages/imagen_page.dart new file mode 100644 index 00000000..bb08a4b5 --- /dev/null +++ b/vertexai/lib/pages/imagen_page.dart @@ -0,0 +1,229 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +//import 'package:firebase_storage/firebase_storage.dart'; +import '../widgets/message_widget.dart'; + +class ImagenPage extends StatefulWidget { + const ImagenPage({ + super.key, + required this.title, + required this.model, + }); + + final String title; + final GenerativeModel model; + + @override + State createState() => _ImagenPageState(); +} + +class _ImagenPageState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(); + final List _generatedContent = []; + bool _loading = false; + late final ImagenModel _imagenModel; + + @override + void initState() { + super.initState(); + var generationConfig = ImagenGenerationConfig( + negativePrompt: 'frog', + numberOfImages: 1, + aspectRatio: ImagenAspectRatio.square1x1, + imageFormat: ImagenFormat.jpeg(compressionQuality: 75), + ); + _imagenModel = FirebaseVertexAI.instance.imagenModel( + model: 'imagen-3.0-generate-001', + generationConfig: generationConfig, + safetySettings: ImagenSafetySettings( + ImagenSafetyFilterLevel.blockLowAndAbove, + ImagenPersonFilterLevel.allowAdult, + ), + ); + } + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + return MessageWidget( + text: _generatedContent[idx].text, + image: _generatedContent[idx].image, + isFromUser: _generatedContent[idx].fromUser ?? false, + ); + }, + itemCount: _generatedContent.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + controller: _textController, + ), + ), + const SizedBox.square( + dimension: 15, + ), + if (!_loading) + IconButton( + onPressed: () async { + await _testImagen(_textController.text); + }, + icon: Icon( + Icons.image_search, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Imagen raw data', + ) + else + const CircularProgressIndicator(), + // NOTE: Keep this API private until future release. + // if (!_loading) + // IconButton( + // onPressed: () async { + // await _testImagenGCS(_textController.text); + // }, + // icon: Icon( + // Icons.imagesearch_roller, + // color: Theme.of(context).colorScheme.primary, + // ), + // tooltip: 'Imagen GCS', + // ) + // else + // const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ), + ); + } + + Future _testImagen(String prompt) async { + setState(() { + _loading = true; + }); + + var response = await _imagenModel.generateImages(prompt); + + if (response.images.isNotEmpty) { + var imagenImage = response.images[0]; + + _generatedContent.add( + MessageData( + image: Image.memory(imagenImage.bytesBase64Encoded), + text: prompt, + fromUser: false, + ), + ); + } else { + // Handle the case where no images were generated + _showError('Error: No images were generated.'); + } + setState(() { + _loading = false; + _scrollDown(); + }); + } + // NOTE: Keep this API private until future release. + // Future _testImagenGCS(String prompt) async { + // setState(() { + // _loading = true; + // }); + // var gcsUrl = 'gs://vertex-ai-example-ef5a2.appspot.com/imagen'; + + // var response = await _imagenModel.generateImagesGCS(prompt, gcsUrl); + + // if (response.images.isNotEmpty) { + // var imagenImage = response.images[0]; + // final returnImageUri = imagenImage.gcsUri; + // final reference = FirebaseStorage.instance.refFromURL(returnImageUri); + // final downloadUrl = await reference.getDownloadURL(); + // // Process the image + // _generatedContent.add( + // MessageData( + // image: Image(image: NetworkImage(downloadUrl)), + // text: prompt, + // fromUser: false, + // ), + // ); + // } else { + // // Handle the case where no images were generated + // _showError('Error: No images were generated.'); + // } + // setState(() { + // _loading = false; + // }); + // } + + void _showError(String message) { + showDialog( + 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'), + ), + ], + ); + }, + ); + } +} diff --git a/vertexai/lib/pages/schema_page.dart b/vertexai/lib/pages/schema_page.dart new file mode 100644 index 00000000..71fff426 --- /dev/null +++ b/vertexai/lib/pages/schema_page.dart @@ -0,0 +1,182 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import '../widgets/message_widget.dart'; + +class SchemaPromptPage extends StatefulWidget { + const SchemaPromptPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _SchemaPromptPageState(); +} + +class _SchemaPromptPageState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(); + final List _messages = []; + bool _loading = false; + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _promptSchemaTest(); + } + : null, + child: const Text('Schema Prompt'), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _promptSchemaTest() async { + setState(() { + _loading = true; + }); + try { + final content = [ + Content.text( + "For use in a children's card game, generate 10 animal-based " + 'characters.', + ), + ]; + + final jsonSchema = Schema.object( + properties: { + 'characters': Schema.array( + items: Schema.object( + properties: { + 'name': Schema.string(), + 'age': Schema.integer(), + 'species': Schema.string(), + 'accessory': + Schema.enumString(enumValues: ['hat', 'belt', 'shoes']), + }, + ), + ), + }, + optionalProperties: ['accessory'], + ); + + final response = await widget.model.generateContent( + content, + generationConfig: GenerationConfig( + responseMimeType: 'application/json', + responseSchema: jsonSchema, + ), + ); + + var text = response.text; + _messages.add(MessageData(text: text, fromUser: false)); + + if (text == null) { + _showError('No response from API.'); + 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( + 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'), + ), + ], + ); + }, + ); + } +} diff --git a/vertexai/lib/pages/token_count_page.dart b/vertexai/lib/pages/token_count_page.dart new file mode 100644 index 00000000..148fc21a --- /dev/null +++ b/vertexai/lib/pages/token_count_page.dart @@ -0,0 +1,106 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import '../widgets/message_widget.dart'; + +class TokenCountPage extends StatefulWidget { + const TokenCountPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _TokenCountPageState(); +} + +class _TokenCountPageState extends State { + final List _messages = []; + bool _loading = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testCountToken(); + } + : null, + child: const Text('Count Tokens'), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _testCountToken() async { + setState(() { + _loading = true; + }); + + const prompt = 'tell a short story'; + final content = Content.text(prompt); + final tokenResponse = await widget.model.countTokens([content]); + final tokenResult = 'Count token: ${tokenResponse.totalTokens}, billable ' + 'characters: ${tokenResponse.totalBillableCharacters}'; + _messages.add(MessageData(text: tokenResult, fromUser: false)); + + final contentResponse = await widget.model.generateContent([content]); + final contentMetaData = 'result metadata, promptTokenCount:' + '${contentResponse.usageMetadata!.promptTokenCount}, ' + 'candidatesTokenCount:' + '${contentResponse.usageMetadata!.candidatesTokenCount}, ' + 'totalTokenCount:' + '${contentResponse.usageMetadata!.totalTokenCount}'; + _messages.add(MessageData(text: contentMetaData, fromUser: false)); + setState(() { + _loading = false; + }); + } +} diff --git a/vertexai/lib/pages/video_page.dart b/vertexai/lib/pages/video_page.dart new file mode 100644 index 00000000..532b9813 --- /dev/null +++ b/vertexai/lib/pages/video_page.dart @@ -0,0 +1,116 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:firebase_vertexai/firebase_vertexai.dart'; +import 'package:flutter/services.dart'; +import '../widgets/message_widget.dart'; + +class VideoPage extends StatefulWidget { + const VideoPage({super.key, required this.title, required this.model}); + + final String title; + final GenerativeModel model; + + @override + State createState() => _VideoPageState(); +} + +class _VideoPageState extends State { + ChatSession? chat; + late final GenerativeModel model; + final List _messages = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + chat = widget.model.startChat(); + } + + Future _testVideo(model) async { + try { + ByteData videoBytes = + await rootBundle.load('assets/videos/landscape.mp4'); + + const _prompt = 'Can you tell me what is in the video?'; + + final prompt = TextPart(_prompt); + + setState(() { + _messages.add(MessageData(text: _prompt, fromUser: true)); + }); + + final videoPart = + InlineDataPart('video/mp4', videoBytes.buffer.asUint8List()); + + final response = await widget.model.generateContent([ + Content.multi([prompt, videoPart]), + ]); + + setState(() { + _messages.add(MessageData(text: response.text, fromUser: false)); + }); + } catch (e) { + print('Error sending video to model: $e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + itemBuilder: (context, idx) { + return MessageWidget( + text: _messages[idx].text, + isFromUser: _messages[idx].fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Center( + child: SizedBox( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testVideo(widget.model); + } + : null, + child: const Text('Test Video Prompt'), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/vertexai/lib/utils/audio_player.dart b/vertexai/lib/utils/audio_player.dart new file mode 100644 index 00000000..3c555948 --- /dev/null +++ b/vertexai/lib/utils/audio_player.dart @@ -0,0 +1,143 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:typed_data'; +import 'dart:async'; + +import 'package:just_audio/just_audio.dart'; + +/// Creates a WAV audio chunk with a properly formatted header. +Future audioChunkWithHeader( + List data, + int sampleRate, +) async { + var channels = 1; + + int byteRate = ((16 * sampleRate * channels) / 8).round(); + + var size = data.length; + var fileSize = size + 36; + + Uint8List header = Uint8List.fromList([ + // "RIFF" + 82, 73, 70, 70, + fileSize & 0xff, + (fileSize >> 8) & 0xff, + (fileSize >> 16) & 0xff, + (fileSize >> 24) & 0xff, + // WAVE + 87, 65, 86, 69, + // fmt + 102, 109, 116, 32, + // fmt chunk size 16 + 16, 0, 0, 0, + // Type of format + 1, 0, + // One channel + channels, 0, + // Sample rate + sampleRate & 0xff, + (sampleRate >> 8) & 0xff, + (sampleRate >> 16) & 0xff, + (sampleRate >> 24) & 0xff, + // Byte rate + byteRate & 0xff, + (byteRate >> 8) & 0xff, + (byteRate >> 16) & 0xff, + (byteRate >> 24) & 0xff, + // Uhm + ((16 * channels) / 8).round(), 0, + // bitsize + 16, 0, + // "data" + 100, 97, 116, 97, + size & 0xff, + (size >> 8) & 0xff, + (size >> 16) & 0xff, + (size >> 24) & 0xff, + // incoming data + ...data, + ]); + return header; +} + +class ByteStreamAudioSource extends StreamAudioSource { + ByteStreamAudioSource(this.bytes) : super(tag: 'Byte Stream Audio'); + + final Uint8List bytes; + + @override + Future request([int? start, int? end]) async { + start ??= 0; + end ??= bytes.length; + return StreamAudioResponse( + sourceLength: bytes.length, + contentLength: end - start, + offset: start, + stream: Stream.value(bytes.sublist(start, end)), + contentType: 'audio/wav', // Or the appropriate content type + ); + } +} + +class AudioStreamManager { + final _audioPlayer = AudioPlayer(); + final _audioChunkController = StreamController(); + var _audioSource = ConcatenatingAudioSource( + children: [], + ); + + AudioStreamManager() { + _initAudioPlayer(); + } + + Future _initAudioPlayer() async { + // 1. Create a ConcatenatingAudioSource to handle the stream + await _audioPlayer.setAudioSource(_audioSource); + + // 2. Listen to the stream of audio chunks + _audioChunkController.stream.listen(_addAudioChunk); + + await _audioPlayer.play(); // Start playing (even if initially empty) + + _audioPlayer.processingStateStream.listen((state) async { + if (state == ProcessingState.completed) { + await _audioPlayer + .pause(); // Or player.stop() if you want to release resources + await _audioPlayer.seek(Duration.zero, index: 0); + await _audioSource.clear(); + await _audioPlayer.play(); + } + }); + } + + Future _addAudioChunk(Uint8List chunk) async { + var buffer = ByteStreamAudioSource(chunk); + + await _audioSource.add(buffer); + } + + void addAudio(Uint8List chunk) { + _audioChunkController.add(chunk); + } + + Future stopAudioPlayer() async { + await _audioPlayer.stop(); + } + + Future disposeAudioPlayer() async { + await _audioPlayer.dispose(); + await _audioChunkController.close(); + } +} diff --git a/vertexai/lib/utils/audio_recorder.dart b/vertexai/lib/utils/audio_recorder.dart new file mode 100644 index 00000000..0d3ca4c2 --- /dev/null +++ b/vertexai/lib/utils/audio_recorder.dart @@ -0,0 +1,241 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:record/record.dart'; + +/// An exception thrown when microphone permission is denied or not granted. +class MicrophonePermissionDeniedException implements Exception { + /// The optional message associated with the permission denial. + final String? message; + + /// Creates a new [MicrophonePermissionDeniedException] with an optional [message]. + MicrophonePermissionDeniedException([this.message]); + + @override + String toString() { + if (message == null) { + return 'MicrophonePermissionDeniedException'; + } + return 'MicrophonePermissionDeniedException: $message'; + } +} + +class Resampler { + /// Resamples 16-bit integer PCM audio data from a source sample rate to a + /// target sample rate using linear interpolation. + /// + /// [sourceRate]: The sample rate of the input audio data. + /// [targetRate]: The desired sample rate of the output audio data. + /// [input]: The input audio data as a Uint8List containing 16-bit PCM samples. + /// + /// Returns a new Uint8List containing 16-bit PCM samples resampled to the + /// target rate. + static Uint8List resampleLinear16( + int sourceRate, + int targetRate, + Uint8List input, + ) { + if (sourceRate == targetRate) return input; // No resampling needed + + final outputLength = (input.length * targetRate / sourceRate).round(); + final output = Uint8List(outputLength); + final inputData = Int16List.view(input.buffer); + final outputData = Int16List.view(output.buffer); + + for (int i = 0; i < outputLength ~/ 2; i++) { + final sourcePosition = i * sourceRate / targetRate; + final index1 = sourcePosition.floor(); + final index2 = index1 + 1; + final weight2 = sourcePosition - index1; + final weight1 = 1.0 - weight2; + + // Ensure indices are within the valid range + final sample1 = inputData[index1.clamp(0, inputData.length - 1)]; + final sample2 = inputData[index2.clamp(0, inputData.length - 1)]; + + // Interpolate and convert back to 16-bit integer + final interpolatedSample = + (sample1 * weight1 + sample2 * weight2).toInt(); + + outputData[i] = interpolatedSample; + } + + return output; + } +} + +class InMemoryAudioRecorder { + final _audioChunks = []; + final _recorder = AudioRecorder(); + StreamSubscription? _recordSubscription; + late String? _lastAudioPath; + AudioEncoder _encoder = AudioEncoder.pcm16bits; + + Future _getPath() async { + String suffix; + if (_encoder == AudioEncoder.pcm16bits) { + suffix = 'pcm'; + } else if (_encoder == AudioEncoder.aacLc) { + suffix = 'm4a'; + } else { + suffix = 'wav'; + } + final dir = await getDownloadsDirectory(); + final path = + '${dir!.path}/audio_${DateTime.now().millisecondsSinceEpoch}.$suffix'; + return path; + } + + Future checkPermission() async { + final hasPermission = await _recorder.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException('Not having mic permission'); + } + } + + Future _isEncoderSupported(AudioEncoder encoder) async { + final isSupported = await _recorder.isEncoderSupported( + encoder, + ); + + if (!isSupported) { + debugPrint('${encoder.name} is not supported on this platform.'); + debugPrint('Supported encoders are:'); + + for (final e in AudioEncoder.values) { + if (await _recorder.isEncoderSupported(e)) { + debugPrint('- ${e.name}'); + } + } + } + + return isSupported; + } + + Future startRecording({bool fromFile = false}) async { + if (!await _isEncoderSupported(_encoder)) { + return; + } + var recordConfig = RecordConfig( + encoder: _encoder, + sampleRate: 16000, + numChannels: 1, + ); + final devs = await _recorder.listInputDevices(); + debugPrint(devs.toString()); + _lastAudioPath = await _getPath(); + if (fromFile) { + await _recorder.start(recordConfig, path: _lastAudioPath!); + } else { + final stream = await _recorder.startStream(recordConfig); + _recordSubscription = stream.listen(_audioChunks.add); + } + } + + Future startRecordingFile() async { + if (!await _isEncoderSupported(_encoder)) { + return; + } + var recordConfig = RecordConfig( + encoder: _encoder, + sampleRate: 16000, + numChannels: 1, + ); + final devs = await _recorder.listInputDevices(); + debugPrint(devs.toString()); + _lastAudioPath = await _getPath(); + await _recorder.start(recordConfig, path: _lastAudioPath!); + } + + Stream startRecordingStream() async* { + if (!await _isEncoderSupported(_encoder)) { + return; + } + var recordConfig = RecordConfig( + encoder: _encoder, + sampleRate: 16000, + numChannels: 1, + ); + final devices = await _recorder.listInputDevices(); + debugPrint(devices.toString()); + final stream = await _recorder.startStream(recordConfig); + + await for (final data in stream) { + yield data; + } + } + + Future stopRecording() async { + await _recordSubscription?.cancel(); + _recordSubscription = null; + + await _recorder.stop(); + } + + Future fetchAudioBytes({ + bool fromFile = false, + bool removeHeader = false, + }) async { + Uint8List resultBytes; + if (fromFile) { + resultBytes = await _getAudioBytesFromFile(_lastAudioPath!); + } else { + final builder = BytesBuilder(); + _audioChunks.forEach(builder.add); + resultBytes = builder.toBytes(); + } + + // resample + resultBytes = Resampler.resampleLinear16(44100, 16000, resultBytes); + final dir = await getDownloadsDirectory(); + final path = '${dir!.path}/audio_resampled.pcm'; + final file = File(path); + final sink = file.openWrite(); + + sink.add(resultBytes); + + await sink.close(); + return resultBytes; + } + + Future _removeWavHeader(Uint8List audio) async { + // Assuming a standard WAV header size of 44 bytes + const wavHeaderSize = 44; + final audioData = audio.sublist(wavHeaderSize); + return audioData; + } + + Future _getAudioBytesFromFile( + String filePath, { + bool removeHeader = false, + }) async { + final file = File(_lastAudioPath!); + + if (!file.existsSync()) { + throw Exception('Audio file not found: ${file.path}'); + } + + var pcmBytes = await file.readAsBytes(); + if (removeHeader) { + pcmBytes = await _removeWavHeader(pcmBytes); + } + return pcmBytes; + } +} diff --git a/vertexai/lib/widgets/message_widget.dart b/vertexai/lib/widgets/message_widget.dart new file mode 100644 index 00000000..b8a0f23c --- /dev/null +++ b/vertexai/lib/widgets/message_widget.dart @@ -0,0 +1,68 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +class MessageData { + MessageData({this.image, this.text, this.fromUser}); + final Image? image; + final String? text; + final bool? fromUser; +} + +class MessageWidget extends StatelessWidget { + final Image? image; + final String? text; + final bool isFromUser; + + const MessageWidget({ + super.key, + this.image, + this.text, + required this.isFromUser, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: + isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 600), + 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: Column( + children: [ + if (text case final text?) MarkdownBody(data: text), + if (image case final image?) image, + ], + ), + ), + ), + ], + ); + } +} diff --git a/vertexai/macos/Flutter/GeneratedPluginRegistrant.swift b/vertexai/macos/Flutter/GeneratedPluginRegistrant.swift index 7bade716..ae59a859 100644 --- a/vertexai/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/vertexai/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,22 @@ import FlutterMacOS import Foundation +import audio_session import firebase_app_check +import firebase_auth import firebase_core +import firebase_storage +import just_audio +import path_provider_foundation +import record_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) + JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) } diff --git a/vertexai/macos/Podfile.lock b/vertexai/macos/Podfile.lock index 19ca6f3e..b6acd317 100644 --- a/vertexai/macos/Podfile.lock +++ b/vertexai/macos/Podfile.lock @@ -1,53 +1,117 @@ PODS: - - AppCheckCore (10.19.0): - - GoogleUtilities/Environment (~> 7.13) - - GoogleUtilities/UserDefaults (~> 7.13) - - PromisesObjC (~> 2.3) - - Firebase/AppCheck (10.24.0): + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - audio_session (0.0.1): + - FlutterMacOS + - Firebase/AppCheck (11.10.0): + - Firebase/CoreOnly + - FirebaseAppCheck (~> 11.10.0) + - Firebase/Auth (11.10.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.10.0) + - Firebase/CoreOnly (11.10.0): + - FirebaseCore (~> 11.10.0) + - Firebase/Storage (11.10.0): - Firebase/CoreOnly - - FirebaseAppCheck (~> 10.24.0) - - Firebase/CoreOnly (10.24.0): - - FirebaseCore (= 10.24.0) - - firebase_app_check (0.2.2-2): - - Firebase/AppCheck (~> 10.24.0) - - Firebase/CoreOnly (~> 10.24.0) + - FirebaseStorage (~> 11.10.0) + - firebase_app_check (0.3.2-5): + - Firebase/AppCheck (~> 11.10.0) + - Firebase/CoreOnly (~> 11.10.0) + - firebase_core + - FlutterMacOS + - firebase_auth (5.5.2): + - Firebase/Auth (~> 11.10.0) + - Firebase/CoreOnly (~> 11.10.0) - firebase_core - FlutterMacOS - - firebase_core (2.30.0): - - Firebase/CoreOnly (~> 10.24.0) + - firebase_core (3.13.0): + - Firebase/CoreOnly (~> 11.10.0) - FlutterMacOS - - FirebaseAppCheck (10.24.0): - - AppCheckCore (~> 10.18) - - FirebaseAppCheckInterop (~> 10.17) - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - PromisesObjC (~> 2.1) - - FirebaseAppCheckInterop (10.24.0) - - FirebaseCore (10.24.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.24.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" + - firebase_storage (12.4.5): + - Firebase/CoreOnly (~> 11.10.0) + - Firebase/Storage (~> 11.10.0) + - firebase_core + - FlutterMacOS + - FirebaseAppCheck (11.10.0): + - AppCheckCore (~> 11.0) + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseCore (~> 11.10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - FirebaseAppCheckInterop (11.12.0) + - FirebaseAuth (11.10.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.10.0) + - FirebaseCoreExtension (~> 11.10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.12.0) + - FirebaseCore (11.10.0): + - FirebaseCoreInternal (~> 11.10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.10.0): + - FirebaseCore (~> 11.10.0) + - FirebaseCoreInternal (11.10.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseStorage (11.10.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.10.0) + - FirebaseCoreExtension (~> 11.10.0) + - GoogleUtilities/Environment (~> 8.0) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) - FlutterMacOS (1.0.0) - - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/AppDelegateSwizzler (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.0): + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - "GoogleUtilities/NSData+zlib (7.13.0)": + - GoogleUtilities/Network (8.0.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): + - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.0) - - GoogleUtilities/UserDefaults (7.13.0): + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (4.4.0) + - just_audio (0.0.1): + - Flutter + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - PromisesObjC (2.4.0) + - record_darwin (1.0.0): + - FlutterMacOS DEPENDENCIES: + - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`) + - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - firebase_storage (from `Flutter/ephemeral/.symlinks/plugins/firebase_storage/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) SPEC REPOS: trunk: @@ -55,32 +119,60 @@ SPEC REPOS: - Firebase - FirebaseAppCheck - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal + - FirebaseStorage - GoogleUtilities + - GTMSessionFetcher - PromisesObjC EXTERNAL SOURCES: + audio_session: + :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos firebase_app_check: :path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos + firebase_auth: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos firebase_core: :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + firebase_storage: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_storage/macos FlutterMacOS: :path: Flutter/ephemeral + just_audio: + :path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + record_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos SPEC CHECKSUMS: - AppCheckCore: 58dbd968d93b7c56650c28a1ac404d7a54307c0e - Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 - firebase_app_check: 5c627cdccbe38b268fd1f347200ac0aaef43c3cb - firebase_core: 15617e716aee891ef3b431cabfa1859261537ecf - FirebaseAppCheck: afb42367002c12bbb5f58c4a954ecd2f0a171182 - FirebaseAppCheckInterop: fecc08c89936c8acb1428d8088313aabedb348e4 - FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 - FirebaseCoreInternal: bcb5acffd4ea05e12a783ecf835f2210ce3dc6af + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + audio_session: 728ae3823d914f809c485d390274861a24b0904e + Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 + firebase_app_check: 9498006ad0fd951c98efff5700e1b87d020675d3 + firebase_auth: 2271b9240513461dfcdcf7cfa0121893acdb2de7 + firebase_core: bb06473757206589a00a36920cbf0f33646e19cc + firebase_storage: a361c42a15990723606a0fe1cdcb7dc6cbeefb53 + FirebaseAppCheck: 9687ebd909702469bc09d2d58008697b83f4ac27 + FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852 + FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32 + FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595 + FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 + FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac + FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 + FirebaseStorage: e83d1b9c8a5318d46ccfb2955f0d98095e0bf598 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + GTMSessionFetcher: 75b671f9e551e4c49153d4c4f8659ef4f559b970 + just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + record_darwin: a0d515a0ef78c440c123ea3ac76184c9927a94d6 PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/vertexai/macos/Runner.xcodeproj/project.pbxproj b/vertexai/macos/Runner.xcodeproj/project.pbxproj index 98b78d95..4bc66a51 100644 --- a/vertexai/macos/Runner.xcodeproj/project.pbxproj +++ b/vertexai/macos/Runner.xcodeproj/project.pbxproj @@ -561,7 +561,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -643,7 +643,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -693,7 +693,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/vertexai/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/vertexai/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b2775746..b0a82f08 100644 --- a/vertexai/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/vertexai/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/vertexai/macos/Runner/AppDelegate.swift b/vertexai/macos/Runner/AppDelegate.swift index d53ef643..b3c17614 100644 --- a/vertexai/macos/Runner/AppDelegate.swift +++ b/vertexai/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/vertexai/pubspec.lock b/vertexai/pubspec.lock index 8ee784e7..7df7a39b 100644 --- a/vertexai/pubspec.lock +++ b/vertexai/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "99b5dec989287c1aca71bf27339e0022b4dc3679225f442fb75790ef44535bf8" + sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422 url: "https://pub.dev" source: hosted - version: "1.3.30" + version: "1.3.54" args: dependency: transitive description: @@ -21,42 +21,58 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" + url: "https://pub.dev" + source: hosted + version: "0.1.25" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -69,66 +85,130 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" firebase_app_check: - dependency: "direct main" + dependency: transitive description: name: firebase_app_check - sha256: "7c875110f78044862858a52ffc7702bf828aa97132fa68e1a79f25daaf71fc2f" + sha256: "9c2b9af9204f5255501127b2e62597ead4003121a93eb385732a43e05fb182e3" url: "https://pub.dev" source: hosted - version: "0.2.2+2" + version: "0.3.2+5" firebase_app_check_platform_interface: dependency: transitive description: name: firebase_app_check_platform_interface - sha256: "9941f04ad2d32620712321cd2c1419687b42a7a2b16d4322afdb6ab36cc23f31" + sha256: bac6ede93128828039f4cf95c5ecd2f7aca0daec41005ec8375b98d8fb470b1c url: "https://pub.dev" source: hosted - version: "0.1.0+24" + version: "0.1.1+5" firebase_app_check_web: dependency: transitive description: name: firebase_app_check_web - sha256: cb3723a19952da7bcec1de0149b617a800c0d018b673099dcc5b89e4424ffc95 + sha256: d9a406cf2e99917aa20ab2c68c350550e5b0bd448d3095f7eeb48c4673d02797 url: "https://pub.dev" source: hosted - version: "0.1.2+2" + version: "0.2.0+9" + firebase_auth: + dependency: transitive + description: + name: firebase_auth + sha256: "54c62b2d187709114dd09ce658a8803ee91f9119b0e0d3fc2245130ad9bff9ad" + url: "https://pub.dev" + source: hosted + version: "5.5.2" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "5402d13f4bb7f29f2fb819f3b6b5a5a56c9f714aef2276546d397e25ac1b6b8e" + url: "https://pub.dev" + source: hosted + version: "7.6.2" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: "2be496911f0807895d5fe8067b70b7d758142dd7fb26485cbe23e525e2547764" + url: "https://pub.dev" + source: hosted + version: "5.14.2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "6b1152a5af3b1cfe7e45309e96fc1aa14873f410f7aadb3878aa7812acfa7531" + sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe" url: "https://pub.dev" source: hosted - version: "2.30.0" + version: "3.13.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: c8b02226e548f35aace298e2bb2e6c24e34e8a203d614e742bb1146e5a4ad3c8 + sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23" + url: "https://pub.dev" + source: hosted + version: "2.22.0" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: b66435730252985c49aabe83e0490bcfab2e7b3f2192bf421ca596fa490de14a + url: "https://pub.dev" + source: hosted + version: "12.4.5" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: "08d32cae58200c34f504098d106952213f2e4c32db111ae7757a86887428ab81" url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "5.2.5" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: d6aee6867f8c369a88484367a8b1f0f6d0f022b4ff2622b51e32dfeef839f9d4 + url: "https://pub.dev" + source: hosted + version: "3.10.12" firebase_vertexai: dependency: "direct main" description: name: firebase_vertexai - sha256: "6e61f6717bee3ab563e8e506e0fed98761f98c181626c62d924d06598786e95e" + sha256: "627c4d9b778d3c7cccc496f2c7200aa142b78485fe3dc6f7b4912b57463003f2" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -138,10 +218,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" flutter_markdown: dependency: "direct main" description: @@ -160,22 +240,14 @@ packages: description: flutter source: sdk version: "0.0.0" - google_generative_ai: - dependency: transitive - description: - name: google_generative_ai - sha256: bb7d3480b05afb3b1f2459b52893cb22f69ded4e2fb853e212437123c457f1be - url: "https://pub.dev" - source: hosted - version: "0.4.0" http: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" http_parser: dependency: transitive description: @@ -184,22 +256,46 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e + url: "https://pub.dev" + source: hosted + version: "0.9.46" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "4cd94536af0219fa306205a58e78d67e02b0555283c1c094ee41e402a14a5c4a" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "8c7e779892e180cbc9ffb5a3c52f6e90e1cbbf4a63694cc450972a7edbd2bb6d" + url: "https://pub.dev" + source: hosted + version: "0.4.15" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -212,10 +308,10 @@ packages: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" markdown: dependency: transitive description: @@ -228,34 +324,90 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -264,59 +416,131 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + record: + dependency: "direct main" + description: + name: record + sha256: "2e3d56d196abcd69f1046339b75e5f3855b2406fc087e5991f6703f188aa03a6" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + record_android: + dependency: transitive + description: + name: record_android + sha256: "36e009c3b83e034321a44a7683d95dd055162a231f95600f7da579dcc79701f9" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + record_darwin: + dependency: transitive + description: + name: record_darwin + sha256: e487eccb19d82a9a39cd0126945cfc47b9986e0df211734e2788c95e3f63c82c + url: "https://pub.dev" + source: hosted + version: "1.2.2" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: "654c08113961051dcb5427e63f56315ba47c0752781ba990dac9313d0ec23c70" + url: "https://pub.dev" + source: hosted + version: "1.1.6" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "26bfebc8899f4fa5b6b044089887dc42115820cd6a907bdf40c16e909e87de0a" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" typed_data: dependency: transitive description: @@ -325,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -337,18 +569,42 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b + url: "https://pub.dev" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/vertexai/pubspec.yaml b/vertexai/pubspec.yaml index 7c44dc23..309f5a56 100644 --- a/vertexai/pubspec.yaml +++ b/vertexai/pubspec.yaml @@ -28,33 +28,25 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + firebase_core: ^3.13.0 + firebase_storage: ^12.4.5 + firebase_vertexai: ^1.5.0 + flutter: + sdk: flutter flutter_markdown: ^0.6.20 - firebase_vertexai: ^0.1.1 - firebase_core: ^2.27.0 - firebase_app_check: ^0.2.2+2 + just_audio: ^0.9.43 + path_provider: ^2.1.5 + record: ^5.2.1 dev_dependencies: + flutter_lints: ^4.0.0 flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^3.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is @@ -63,3 +55,5 @@ flutter: uses-material-design: true assets: - assets/images/ + - assets/documents/ + - assets/videos/