diff --git a/firebase_ai_logic_showcase/.gitignore b/firebase_ai_logic_showcase/.gitignore new file mode 100644 index 0000000..df0e787 --- /dev/null +++ b/firebase_ai_logic_showcase/.gitignore @@ -0,0 +1,54 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# macOS +ephemeral/ + +#firebase +google-services.json +GoogleService-Info.plist +.firebaserc diff --git a/firebase_ai_logic_showcase/.idx/dev.nix b/firebase_ai_logic_showcase/.idx/dev.nix new file mode 100644 index 0000000..919294f --- /dev/null +++ b/firebase_ai_logic_showcase/.idx/dev.nix @@ -0,0 +1,61 @@ +# To learn more about how to use Nix to configure your environment +# see: https://firebase.google.com/docs/studio/customize-workspace +{ pkgs, ... }: { + # Which nixpkgs channel to use. + channel = "stable-24.05"; # or "unstable" + # Use https://search.nixos.org/packages to find packages + packages = [ + pkgs.jdk21 + pkgs.unzip + pkgs.nodejs_22 + pkgs.nodePackages.nodemon + ]; + # Sets environment variables in the workspace + env = { + # Enable AppCheck for additional security for critical endpoints. + # Follow the configuration steps in the README to set up your project. + # ENABLE_APPCHECK = "TRUE"; + LOCAL_RECOMMENDATION_SERVICE = "http://127.0.0.1:8084"; + GOOGLE_PROJECT = ""; + CLOUDSDK_CORE_PROJECT = ""; + TF_VAR_project = ""; + # Flip to true to help improve Angular + NG_CLI_ANALYTICS = "false"; + # Quieter Terraform logs + TF_IN_AUTOMATION = "true"; + }; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" + extensions = [ + "hashicorp.terraform" + "ms-vscode.js-debug" + "Dart-Code.flutter" + "Dart-Code.dart-code" + ]; + workspace = { + # Runs when a workspace is first created with this `dev.nix` file + onCreate = { + npm-install = "flutter pub get"; + default.openFiles = [ + "README.md" + "lib/main.dart" + ]; + }; + # To run something each time the workspace is (re)started, use the `onStart` hook + }; + # Enable previews and customize configuration + previews = { + enable = true; + previews = { + web = { + command = [ "flutter" "run" "--machine" "-d" "web-server" "--web-hostname" "0.0.0.0" "--web-port" "$PORT" ]; + manager = "flutter"; + }; + # android = { + # command = [ "flutter" "run" "--machine" "-d" "android" "-d" "localhost:5555" ]; + # manager = "flutter"; + # }; + }; + }; + }; +} diff --git a/firebase_ai_logic_showcase/.idx/integrations.json b/firebase_ai_logic_showcase/.idx/integrations.json new file mode 100644 index 0000000..b9b8a61 --- /dev/null +++ b/firebase_ai_logic_showcase/.idx/integrations.json @@ -0,0 +1,9 @@ +{ + "firebase_hosting": {}, + "cloud_run_deploy": { + "region": "us-central1", + "sourceFlag": "--source services/cloud-run", + "allowUnauthenticatedInvocationsFlag": "--allow-unauthenticated" + }, + "gemini_api": {} + } diff --git a/firebase_ai_logic_showcase/.metadata b/firebase_ai_logic_showcase/.metadata new file mode 100644 index 0000000..05a8ab4 --- /dev/null +++ b/firebase_ai_logic_showcase/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "05db9689081f091050f01aed79f04dce0c750154" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: android + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: ios + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: linux + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: macos + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: web + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: windows + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/firebase_ai_logic_showcase/LICENSE.txt b/firebase_ai_logic_showcase/LICENSE.txt new file mode 100644 index 0000000..e58143f --- /dev/null +++ b/firebase_ai_logic_showcase/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 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. diff --git a/firebase_ai_logic_showcase/README.md b/firebase_ai_logic_showcase/README.md new file mode 100644 index 0000000..b302099 --- /dev/null +++ b/firebase_ai_logic_showcase/README.md @@ -0,0 +1,113 @@ +# flutter_firebase_ai_sample +**Target Platforms:** iOS, Android, Web + +**Tech Stack:** [Flutter](https://flutter.dev/) (frontend), +[Firebase AI Logic](https://firebase.google.com/docs/ai-logic) + +![Flutter & Firebase AI Sample App Mobile Screenshots](README/flutter_firebase_ai_sample_hero.png) + +This Flutter application demonstrates Firebase AI Logic capabilities through a +series of interactive demos. Firebase AI Logic provides access to the Gemini and +Imagen family of models, enabling developers to build AI-powered experiences in +Flutter apps. + +> [!NOTE] +> Check out this Google I/O 2025 talk for a full walkthrough on Firebase AI Logic: +> [How to build agentic apps with Flutter and Firebase AI Logic](https://www.youtube.com/watch?v=xo271p-Fl_4). + +## Getting Started + +1. Follow [these instructions](https://firebase.google.com/docs/ai-logic/get-started?&api=vertex#set-up-firebase) +to set up a Firebase project & connect the app to Firebase using `flutterfire configure` + +1. Run `flutter pub get` in the root of the project directory `flutter_ai` to +install the Flutter app dependencies + +1. Run `flutter run -d ` to start the app on iOS, Android, or Web. + +> [!TIP] +> Get available devices by running `flutter devices` ex: `AA8A7357`, `macos`, `chrome`. + +`main.dart` is the entry point for the app, but the `lib/flutter_firebase_ai_demo.dart` +file serves as the table of contents for the various demos. It defines the +structure for each demo and presents them in a navigable list on the app's home screen. + +## Explore the interactive demos: + +### Live API +Real-time bidirectional audio and video streaming with Gemini, demonstrating +dynamic and interactive AI communication. +- **Start/End Call:** Tap the "Call" button (phone icon) to initiate or terminate +the real-time audio and video stream with Gemini. +- **Toggle Video:** Once a call is active, tap the "Video" button (camera icon) +to start or stop sending your camera feed. +- **Flip Camera:** If video is active and multiple cameras are available, use +the "Flip Camera" button to switch between them. +- **Mute Audio:** During a call, tap the "Mute" button to toggle your +microphone's audio input. +- **Function Calling:** This demo is integrated with Function Calling, so +you can ask Gemini to use the two tools that are built into the demo: generate +an image or change the color of the app. + +### Imagen +Generate images directly from text prompts, showcasing generative AI in visual +content creation. +- **Enter a prompt:** Type a description of the image you want to generate into +the text input field. +- **Generate images:** Tap the "Generate" button to send your prompt to the +Imagen model. +- **View results:** The generated images will appear in the display area. +A loading indicator will be shown while the images are being generated. + +### Multimodal Prompt +Interact with Gemini by asking questions about images, audio, video, or text files, +highlighting the model's ability to process diverse inputs. +- **Select a file:** Tap the "Pick File" button to choose an image, audio, video, +or text file from your device. +- **Enter a prompt:** Type your question or request about the selected file into +the text input field. +- **Ask Gemini:** Tap the "Ask Gemini" button to send the file and your prompt +to the Gemini model. +- **View response:** The response from Gemini will appear in the output display +area. A loading indicator will be shown while Gemini is processing your request. + +### Chat with Function Calling +Engage in a continuous conversation with Gemini, where the model maintains +conversation history and uses function calling to perform actions or retrieve information. +- **Switch models:** Use the dropdown menu at the top of the screen to switch +between different Gemini models. +- **Type a message:** Enter your message in the input field at the bottom of the screen. +- **Send a message:** Tap the "Send" button to send your message to Gemini. +- **Attach an image (optional):** Tap the "Image" icon to select an image +from your gallery to send with your message. +- **View conversation:** Your messages and Gemini's responses will appear in the chat history. +- **Use tools with function calling:** With `gemini-2.5-flash` (selected by default), +you can ask Gemini to use the two tools that are built into the demo: + - Generate an image using Imagen (e.g., "generate an image of a cat") or + - Change the color of the app (e.g., "change the color to blue"). +- **Nano Banana** With `gemini-2.5-flash-image`, you can generate and edit images. + - **Generate an image:** Enter a text prompt and generate a new image. + - **Edit an image:** Provide instructions for Gemini to edit a previously generated image or select one from your photo library. + +## Implementation +All Firebase AI Logic code has been separated from the Flutter UI code to make +the code easier to read and understand. For each demo, you will find all of the +encapsulated Firebase AI Logic code in their respective `firebase__service.dart` files. +These files can be found in their respective demo directories, with the exception of +the `ImagenService` which is shared across 3 demos: Live API, Chat, and Imagen +so the code is instead located in `lib/shared/firebaseai_imagen_service.dart`. + +Check out [this table](https://firebase.google.com/docs/ai-logic/models) for +more info on Firebase AI Logic's supported models & features. + +## Additional Resources +- [Firebase AI Logic docs](https://firebase.google.com/docs/ai-logic) +- [[Codelab] Build a Gemini powered Flutter app with Flutter & Firebase AI Logic](https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist) + +Feeling inspired? Check out these other Flutter & Firebase AI Logic sample apps! +- [Agentic App Manager](https://github.com/flutter/demos/tree/main/agentic_app_manager): +Build an agentic experience in a Flutter app using Firebase AI Logic. +- [Colorist](https://github.com/flutter/demos/tree/main/vertex_ai_firebase_flutter_app): +Explore LLM tooling interfaces by allowing users to describe colors in natural language. +The app uses Gemini LLM to interpret descriptions and change the color of a +displayed square by calling specialized color tools. diff --git a/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png b/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png new file mode 100644 index 0000000..8bd3877 Binary files /dev/null and b/firebase_ai_logic_showcase/README/flutter_firebase_ai_sample_hero.png differ diff --git a/firebase_ai_logic_showcase/analysis_options.yaml b/firebase_ai_logic_showcase/analysis_options.yaml new file mode 100644 index 0000000..8957fb3 --- /dev/null +++ b/firebase_ai_logic_showcase/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_relative_imports: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/firebase_ai_logic_showcase/android/.gitignore b/firebase_ai_logic_showcase/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/firebase_ai_logic_showcase/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/firebase_ai_logic_showcase/android/app/build.gradle.kts b/firebase_ai_logic_showcase/android/app/build.gradle.kts new file mode 100644 index 0000000..0281b3e --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.flutter_firebase_ai_sample" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.flutter_firebase_ai_sample" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + ndkVersion = "27.0.12077973" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dc3eede --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt new file mode 100644 index 0000000..8b08e14 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/flutter_firebase_ai_sample/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_firebase_ai_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt new file mode 100644 index 0000000..8b08e14 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/kotlin/com/example/multimodal_ai_prototype/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.flutter_firebase_ai_sample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml b/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml b/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/firebase_ai_logic_showcase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml b/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml b/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml b/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/firebase_ai_logic_showcase/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/firebase_ai_logic_showcase/android/build.gradle.kts b/firebase_ai_logic_showcase/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/firebase_ai_logic_showcase/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/firebase_ai_logic_showcase/android/gradle.properties b/firebase_ai_logic_showcase/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/firebase_ai_logic_showcase/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties b/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/firebase_ai_logic_showcase/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/firebase_ai_logic_showcase/android/settings.gradle.kts b/firebase_ai_logic_showcase/android/settings.gradle.kts new file mode 100644 index 0000000..9e2d35c --- /dev/null +++ b/firebase_ai_logic_showcase/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/firebase_ai_logic_showcase/assets/firebase-ai-logic.png b/firebase_ai_logic_showcase/assets/firebase-ai-logic.png new file mode 100644 index 0000000..46d73b6 Binary files /dev/null and b/firebase_ai_logic_showcase/assets/firebase-ai-logic.png differ diff --git a/firebase_ai_logic_showcase/assets/gemini-logo.png b/firebase_ai_logic_showcase/assets/gemini-logo.png new file mode 100644 index 0000000..3adcf7c Binary files /dev/null and b/firebase_ai_logic_showcase/assets/gemini-logo.png differ diff --git a/firebase_ai_logic_showcase/idx-template.json b/firebase_ai_logic_showcase/idx-template.json new file mode 100644 index 0000000..0406147 --- /dev/null +++ b/firebase_ai_logic_showcase/idx-template.json @@ -0,0 +1,28 @@ +{ + "name": "Flutter app with Firebase AI logic", + "description": "A sample flutter app that demonstrates how to use the Gemini API with Firebase AI Logic", + "categories": [ + "AI & ML", + "Web", + "Firebase", + "Flutter", + "Android", + "iOS" + ], + "icon_image_url": "https://www.gstatic.com/monospace/240513/logo_firebase.svg", + "publisher": "Google LLC", + "params": [ + { + "id": "projectId", + "name": "Firebase Project ID", + "type": "string", + "required": true + }, + { + "id": "bootstrapJs", + "name": "Sample App Bootstrap", + "type": "string", + "required": false + } + ] +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/idx-template.nix b/firebase_ai_logic_showcase/idx-template.nix new file mode 100644 index 0000000..6bb80d1 --- /dev/null +++ b/firebase_ai_logic_showcase/idx-template.nix @@ -0,0 +1,20 @@ +{ pkgs, projectId, bootstrapJs, ... }: +{ + bootstrap = '' + cp -rf ${./.} "$out/" + chmod -R +w "$out" + echo 'bootstrapJs was set to: ${bootstrapJs}' + # Apply project ID to configs + if [ -z '${bootstrapJs}' ] || [ '${bootstrapJs}' = 'false' ] + then + sed -e 's//${projectId}/' ${.idx/dev.nix} > "$out/.idx/dev.nix" + else + sed -e 's//${projectId}/' ${.idx/dev.nix} | sed -e 's/terraform init/# terraform init/' | sed -e 's/terraform apply/# terraform apply/' > "$out/.idx/dev.nix" + echo '${bootstrapJs}' > "$out/lib/bootstrap.js" + echo '{"projects":{"default":"${projectId}"}}' > "$out/.firebaserc" + fi + # Remove the template files themselves and any connection to the template's + # Git repository + rm -rf "$out/.git" "$out/idx-template".{nix,json} "$out/node_modules" + ''; +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/ios/.gitignore b/firebase_ai_logic_showcase/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist b/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig b/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig b/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/firebase_ai_logic_showcase/ios/Podfile b/firebase_ai_logic_showcase/ios/Podfile new file mode 100644 index 0000000..236ec2d --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.0' #FlutterFire plugins require minimum of iOS 15 https://firebase.google.com/docs/flutter/setup?platform=ios + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0f11db6 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,753 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8E6CD9C6541A3CF46C64E1C9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + BA6D30E40E8F68B06E56229C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F9CFD1A5C79D4AABA9C9A108 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 37FD498E274777199498BC21 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2E93BD09C0B0FFDB52311EAA /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3760F5D70F4BD5A3F3B08C19 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3044D5D4F7C58D0A98D41E15 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12DB00086E2FD750FC10BCD2 /* Pods_Runner.framework */, + 66B6A3C553F8B7E90334CF84 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 6B0AAB17435D3F0DE252CF55 /* Pods */ = { + isa = PBXGroup; + children = ( + BA6D30E40E8F68B06E56229C /* Pods-Runner.debug.xcconfig */, + F9CFD1A5C79D4AABA9C9A108 /* Pods-Runner.release.xcconfig */, + 8E6CD9C6541A3CF46C64E1C9 /* Pods-Runner.profile.xcconfig */, + 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */, + E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */, + 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 6B0AAB17435D3F0DE252CF55 /* Pods */, + 3044D5D4F7C58D0A98D41E15 /* Frameworks */, + 9F4B329A2B7756919F9BCADD /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 04715B1AF939C799C4D37CEC /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 37FD498E274777199498BC21 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + E56B7B1A5FD7695313B2FE04 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E65799E83B05311337559E13 /* [CP] Embed Pods Frameworks */, + F554392F020C8FA33594A554 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + EF7DAC43BEEDC80B2E3D9979 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 04715B1AF939C799C4D37CEC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + E56B7B1A5FD7695313B2FE04 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E65799E83B05311337559E13 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F554392F020C8FA33594A554 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2B33F176EE317BBD897D8C8E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E9425AF2FD1A44E278C01648 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2D957529EFAEE0BDD3A76F18 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.multimodalAiPrototype; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift b/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Info.plist b/firebase_ai_logic_showcase/ios/Runner/Info.plist new file mode 100644 index 0000000..31531a5 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Multimodal Ai Prototype + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_firebase_ai_sample + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSMicrophoneUsageDescription + Need to record user voice to communicate with Gemini + NSCameraUsageDescription + Need camera to show Gemini live video stream + NSPhotoLibraryUsageDescription + Need pick images in chat + + diff --git a/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h b/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/firebase_ai_logic_showcase/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift b/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/firebase_ai_logic_showcase/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart b/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart new file mode 100644 index 0000000..f77a65d --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/chat_demo.dart @@ -0,0 +1,223 @@ +// 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:developer'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:permission_handler/permission_handler.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import './ui_components/ui_components.dart'; +import './firebaseai_chat_service.dart'; +import 'ui_components/model_picker.dart'; +import './models/models.dart'; + +class ChatDemo extends ConsumerStatefulWidget { + const ChatDemo({super.key}); + + @override + ConsumerState createState() => _ChatDemoState(); +} + +class _ChatDemoState extends ConsumerState { + // Service for interacting with the Gemini API. + late final ChatService _chatService; + + // UI State + final List _messages = []; + final TextEditingController _userTextInputController = + TextEditingController(); + Uint8List? _attachment; + final ScrollController _scrollController = ScrollController(); + bool _loading = false; + + @override + void initState() { + super.initState(); + _chatService = ChatService(ref); + _chatService.init(); + _userTextInputController.text = geminiModels.selectedModel.defaultPrompt; + } + + @override + void didChangeDependencies() { + requestPermissions(); + super.didChangeDependencies(); + } + + @override + void dispose() { + _scrollController.dispose(); + _userTextInputController.dispose(); + super.dispose(); + } + + Future requestPermissions() async { + if (!kIsWeb) { + await Permission.manageExternalStorage.request(); + } + } + + void _scrollToEnd() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + void _pickImage() async { + final pickedImage = await ImagePicker().pickImage( + source: ImageSource.gallery, + ); + + if (pickedImage != null) { + final imageBytes = await pickedImage.readAsBytes(); + setState(() { + _attachment = imageBytes; + }); + log('attachment saved!'); + } + } + + void sendMessage(String text) async { + if (text.isEmpty) return; + + setState(() { + _loading = true; + }); + + // Add user message to UI + final userMessageText = text.trim(); + final userAttachment = _attachment; + _messages.add( + MessageData( + text: userMessageText, + image: userAttachment != null ? Image.memory(userAttachment) : null, + fromUser: true, + ), + ); + setState(() { + _attachment = null; + _userTextInputController.clear(); + }); + _scrollToEnd(); + + // Construct the Content object for the service + final content = (userAttachment != null) + ? Content.multi([ + TextPart(userMessageText), + InlineDataPart('image/jpeg', userAttachment), + ]) + : Content.text(userMessageText); + + // Call the service and handle the response + try { + final chatResponse = await _chatService.sendMessage(content); + _messages.add( + MessageData( + text: chatResponse.text, + image: chatResponse.image, + fromUser: false, + ), + ); + } catch (e) { + print(e); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text( + 'Oops. Sorry, the message was unable to be sent!', + ), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } finally { + setState(() { + _loading = false; + }); + _scrollToEnd(); + } + } + + void showModelPicker() { + showDialog( + context: context, + builder: (context) { + return ModelPicker( + selectedModel: geminiModels.selectedModel, + onSelected: (value) { + _chatService.changeModel(value); + setState(() { + _userTextInputController.text = + geminiModels.selectedModel.defaultPrompt; + _messages.clear(); + }); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + title: const Text('Chat Demo'), + actions: [ + IconButton( + onPressed: showModelPicker, + icon: Icon(Icons.settings_outlined), + ), + ], + ), + body: AppFrame( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: MessageListView( + messages: _messages, + scrollController: _scrollController, + ), + ), + if (_loading) const LinearProgressIndicator(), + AttachmentPreview(attachment: _attachment), + ], + ), + ), + ), + ), + bottomNavigationBar: MessageInputBar( + textController: _userTextInputController, + loading: _loading, + sendMessage: sendMessage, + onPickImagePressed: _pickImage, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/firebaseai_chat_service.dart b/firebase_ai_logic_showcase/lib/demos/chat/firebaseai_chat_service.dart new file mode 100644 index 0000000..ed5743f --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/firebaseai_chat_service.dart @@ -0,0 +1,121 @@ +// 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:developer'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../shared/app_state.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import './models/models.dart'; + +/// A service that handles all communication with the Firebase AI Gemini API +/// for the Chat Demo. +/// +/// This service demonstrates how to use the `startChat()` method on a +/// `GenerativeModel` to create a persistent conversation. The `ChatSession` +/// object automatically handles the conversation history, making it easy to +/// build multi-turn chat experiences. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/chat?api=dev +class ChatService { + final WidgetRef _ref; + ChatService(this._ref); + + GeminiModel? _gemini = geminiModels.selectedModel; + late ChatSession _chat; + + void init() { + var gemini = _gemini; + if (gemini != null) { + _chat = gemini.model.startChat(); + } + } + + void changeModel(String modelName) { + _gemini = geminiModels.selectModel(modelName); + init(); + } + + Future sendMessage(Content message) async { + try { + var response = await _chat.sendMessage(message); + + if (response.functionCalls.isNotEmpty) { + return _handleFunctionCall(response.functionCalls); + } else { + if (response.inlineDataParts.isNotEmpty) { + final imageBytes = response.inlineDataParts.first.bytes; + var image = Image.memory(imageBytes); + return ChatResponse(text: response.text, image: image); + } + + return ChatResponse(text: response.text); + } + } catch (e) { + log('Error sending message: $e'); + rethrow; + } + } + + Future _handleFunctionCall( + Iterable functionCalls, + ) async { + var functionCall = functionCalls.first; + log("Gemini made a function call: ${functionCall.name}"); + + switch (functionCall.name) { + case 'SetAppColor': + final response = await _handleSetAppColor(functionCall); + return ChatResponse(text: response.text); + case 'GenerateImage': + return await _handleGenerateImage(functionCall); + default: + final response = await _chat.sendMessage( + Content.text( + 'Function Call name was not found! Please try another function call.', + ), + ); + return ChatResponse(text: response.text); + } + } + + Future _handleSetAppColor( + FunctionCall functionCall, + ) async { + log('Set app color!'); + int red = functionCall.args['red']! as int; + int green = functionCall.args['green']! as int; + int blue = functionCall.args['blue']! as int; + var newSeedColor = Color.fromRGBO(red, green, blue, 1); + var executedFunctionCall = _ref + .read(appStateProvider) + .setAppColor(newSeedColor); + return await _chat.sendMessage(Content.text(executedFunctionCall)); + } + + Future _handleGenerateImage(FunctionCall functionCall) async { + log('Generate image!'); + String description = functionCall.args['description']! as String; + var imageBytes = await ImagenService().generateImage(description); + var response = await _chat.sendMessage( + Content.text( + 'Successfully generated an image of $description! Please send back a message to include with the image.', + ), + ); + var responseImage = Image.memory(imageBytes); + return ChatResponse(text: response.text, image: responseImage); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart b/firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart new file mode 100644 index 0000000..ebdb9de --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/models/chat_response.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +/// A simple container for the response from the ChatService. +class ChatResponse { + final String? text; + final Image? image; + + ChatResponse({this.text, this.image}); +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/models/gemini_model.dart b/firebase_ai_logic_showcase/lib/demos/chat/models/gemini_model.dart new file mode 100644 index 0000000..70e882a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/models/gemini_model.dart @@ -0,0 +1,70 @@ +import 'package:firebase_ai/firebase_ai.dart'; +import '../../../shared/function_calling/tools.dart'; + +var geminiModels = GeminiModels(); + +class GeminiModel { + final String name; + final String description; + final GenerativeModel model; + final String defaultPrompt; + + GeminiModel({ + required this.name, + required this.description, + required this.model, + required this.defaultPrompt, + }); +} + +class GeminiModels { + String selectedModelName = 'gemini-2.5-flash'; + GeminiModel get selectedModel => models[selectedModelName]!; + + /// A map of Gemini models that can be used in the Chat Demo. + Map models = { + 'gemini-2.5-flash': GeminiModel( + name: 'gemini-2.5-flash', + description: + 'Our thinking model that offers great, well-rounded capabilities. It\'s designed to offer a balance between price and performance.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + tools: [ + Tool.functionDeclarations([setAppColorTool, generateImageTool]), + ], + generationConfig: GenerationConfig( + responseModalities: [ResponseModalities.text], + ), + ), + defaultPrompt: 'Hey Gemini! Can you set the app color to purple?', + ), + 'gemini-2.5-flash-image-preview': GeminiModel( + name: 'gemini-2.5-flash-image-preview', + description: + 'Our standard Flash model upgraded for rapid creative workflows with image generation and conversational, multi-turn editing capabilities.', + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash-image-preview', + generationConfig: GenerationConfig( + responseModalities: [ + ResponseModalities.text, + ResponseModalities.image, + ], + ), + ), + defaultPrompt: + 'Hey Gemini! Can you create an image of Dash, the Flutter mascot, surfing in Waikiki Hawaii?', + ), + }; + + GeminiModel selectModel(String modelName) { + if (models.containsKey(modelName)) { + selectedModelName = modelName; + } else { + throw Exception('Model $modelName not found'); + } + return selectedModel; + } + + List get modelNames => models.keys.toList(); + GeminiModel operator [](String name) => models[name]!; +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/models/models.dart b/firebase_ai_logic_showcase/lib/demos/chat/models/models.dart new file mode 100644 index 0000000..bebb95e --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/models/models.dart @@ -0,0 +1,2 @@ +export './chat_response.dart'; +export './gemini_model.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/attachment_preview.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/attachment_preview.dart new file mode 100644 index 0000000..18fed23 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/attachment_preview.dart @@ -0,0 +1,47 @@ +// 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 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class AttachmentPreview extends StatelessWidget { + final Uint8List? attachment; + + const AttachmentPreview({super.key, this.attachment}); + + @override + Widget build(BuildContext context) { + return attachment != null + ? Row( + children: [ + Padding( + padding: const EdgeInsets.only(top: AppSpacing.s16), + child: Container( + height: 95, + width: 95, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s8), + image: DecorationImage( + fit: BoxFit.cover, + image: MemoryImage(attachment!), + ), + ), + ), + ), + ], + ) + : Container(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_bubble.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_bubble.dart new file mode 100644 index 0000000..def2442 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_bubble.dart @@ -0,0 +1,70 @@ +// 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_animate/flutter_animate.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import './message_widget.dart'; + +class MessageBubble extends StatelessWidget { + final MessageData message; + + const MessageBubble({super.key, required this.message}); + + @override + Widget build(BuildContext context) { + final isFromUser = message.fromUser ?? false; + return ListTile( + minVerticalPadding: 4, + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadiusGeometry.circular(16), + ), + contentPadding: isFromUser + ? EdgeInsets.only(left: 16, top: 8, right: 8, bottom: 8) + : EdgeInsets.only(left: 8, top: 8, right: 16, bottom: 8), + leading: (!isFromUser) + ? CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.asset('assets/gemini-logo.png'), + ) + : null, + title: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.surfaceBright + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + if (message.text != null) + Align( + alignment: Alignment.centerLeft, + child: MarkdownBody(data: message.text!), + ), + if (message.image != null) + Padding( + padding: EdgeInsets.only(top: 8), + child: ClipRSuperellipse( + borderRadius: BorderRadius.circular(16), + child: message.image!, + ), + ), + ], + ), + ), + ).animate().fadeIn().slideY().scaleXY(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_input_bar.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_input_bar.dart new file mode 100644 index 0000000..1ecd2d6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_input_bar.dart @@ -0,0 +1,93 @@ +// 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 '../../../shared/ui/app_spacing.dart'; + +class MessageInputBar extends StatelessWidget { + final TextEditingController textController; + final bool loading; + final void Function(String) sendMessage; + final VoidCallback onPickImagePressed; + + const MessageInputBar({ + super.key, + required this.textController, + required this.loading, + required this.sendMessage, + required this.onPickImagePressed, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(AppSpacing.s16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1, + color: Theme.of(context).colorScheme.outline.withAlpha(125), + ), + ), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.maybeViewInsetsOf(context)?.bottom ?? 0, + ), + child: SafeArea( + child: Row( + children: [ + IconButton( + onPressed: onPickImagePressed, + icon: const Icon(Icons.image), + ), + const SizedBox.square(dimension: AppSpacing.s8), + Expanded( + child: TextField( + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + }, + controller: textController, + minLines: 2, + maxLines: 2, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: const BorderSide(width: 0), + borderRadius: BorderRadius.all( + Radius.circular(AppSpacing.s8), + ), + ), + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + ), + ), + ), + const SizedBox.square(dimension: AppSpacing.s16), + IconButton.filled( + onPressed: !loading + ? () => sendMessage(textController.text) + : null, + icon: const Icon(Icons.arrow_upward_rounded), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_list_view.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_list_view.dart new file mode 100644 index 0000000..b78d8a0 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_list_view.dart @@ -0,0 +1,40 @@ +// 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 './message_bubble.dart'; +import './message_widget.dart'; + +class MessageListView extends StatelessWidget { + final List messages; + final ScrollController scrollController; + + const MessageListView({ + super.key, + required this.messages, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + controller: scrollController, + itemCount: messages.length, + itemBuilder: (context, idx) { + final message = messages[idx]; + return MessageBubble(message: message); + }, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_widget.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_widget.dart new file mode 100644 index 0000000..92fdf86 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/message_widget.dart @@ -0,0 +1,78 @@ +// 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'; +import 'package:firebase_ai/firebase_ai.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class FunctionCallResponse { + final GenerateContentResponse? response; + final Image? image; + + FunctionCallResponse(this.response, this.image); +} + +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(AppSpacing.s16), + ), + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s16, + horizontal: AppSpacing.s24, + ), + margin: const EdgeInsets.only(bottom: AppSpacing.s8), + child: Column( + children: [ + if (text case final text?) MarkdownBody(data: text), + if (image case final image?) image, + ], + ), + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/model_picker.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/model_picker.dart new file mode 100644 index 0000000..c1915ee --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/model_picker.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/gemini_model.dart'; + +class ModelPicker extends StatefulWidget { + const ModelPicker({ + required this.selectedModel, + required this.onSelected, + super.key, + }); + + final GeminiModel selectedModel; + final Function(String value) onSelected; + + @override + State createState() => _ModelPickerState(); +} + +class _ModelPickerState extends State { + late String _selectedModelName; + late String _selectedModelDescription; + + @override + void initState() { + super.initState(); + _selectedModelName = widget.selectedModel.name; + _selectedModelDescription = widget.selectedModel.description; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Theme.of(context).colorScheme.surface, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DropdownMenu( + label: const Text('Select a Gemini Model'), + initialSelection: _selectedModelName, + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + ), + fillColor: Theme.of(context).colorScheme.primaryContainer, + ), + dropdownMenuEntries: geminiModels.models.entries + .map( + (entry) => + DropdownMenuEntry(value: entry.key, label: entry.key), + ) + .toList(), + onSelected: (value) { + if (value != null) { + setState(() { + _selectedModelName = value; + _selectedModelDescription = + geminiModels[_selectedModelName].description; + }); + widget.onSelected(value); + } + }, + ), + const SizedBox.square(dimension: AppSpacing.s8), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: Text(_selectedModelDescription), + ), + ], + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/chat/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/ui_components.dart new file mode 100644 index 0000000..8572431 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/chat/ui_components/ui_components.dart @@ -0,0 +1,18 @@ +// 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. + +export 'attachment_preview.dart'; +export 'message_input_bar.dart'; +export 'message_list_view.dart'; +export 'message_widget.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/imagen_demo.dart b/firebase_ai_logic_showcase/lib/demos/imagen/imagen_demo.dart new file mode 100644 index 0000000..90b5639 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/imagen_demo.dart @@ -0,0 +1,98 @@ +// 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 'dart:typed_data'; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import './ui_components/ui_components.dart'; + +class ImagenDemo extends StatefulWidget { + const ImagenDemo({super.key}); + + @override + State createState() => _ImagenDemoState(); +} + +class _ImagenDemoState extends State { + // Service for interacting with the Gemini API. + final _imagenService = ImagenService(); + + // UI State + bool _loading = false; + List images = []; + TextEditingController promptController = TextEditingController( + text: + 'Hot air balloons rising over the San Francisco Bay at golden hour ' + 'with a view of the Golden Gate Bridge. Make it anime style.', + ); + + void generateImages(BuildContext context, String prompt) async { + setState(() { + _loading = true; + images = []; // Clear previous images while loading + }); + + try { + final newImages = await _imagenService.generateImages( + prompt, + numberOfImages: 4, + ); + setState(() { + images = newImages; + }); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toString()), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } finally { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Imagen Demo')), + body: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: AppFrame( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: Column( + children: [ + ImageDisplay(loading: _loading, images: images), + const SizedBox.square(dimension: AppSpacing.s8), + PromptInput( + promptController: promptController, + loading: _loading, + generateImages: generateImages, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/image_display.dart b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/image_display.dart new file mode 100644 index 0000000..10013bf --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/image_display.dart @@ -0,0 +1,54 @@ +// 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 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class ImageDisplay extends StatelessWidget { + final bool loading; + final List images; + + const ImageDisplay({super.key, required this.loading, required this.images}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints.loose( + Size(double.infinity, constraints.maxWidth), + ), + child: Center( + child: loading + ? CircularProgressIndicator() + : images.isEmpty + ? Text('Write a prompt below to generate images.') + : CarouselView.weighted( + enableSplash: false, + itemSnapping: true, + flexWeights: [1, 6, 1], + children: images + .map((image) => Image.memory(image)) + .toList(), + ), + ), + ); + }, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/prompt_input.dart b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/prompt_input.dart new file mode 100644 index 0000000..b203ba2 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/prompt_input.dart @@ -0,0 +1,92 @@ +// 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 '../../../shared/ui/app_spacing.dart'; + +class PromptInput extends StatelessWidget { + final TextEditingController promptController; + final bool loading; + final void Function(BuildContext, String) generateImages; + + const PromptInput({ + super.key, + required this.promptController, + required this.loading, + required this.generateImages, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: TextField( + decoration: InputDecoration( + label: const Text('Prompt'), + fillColor: Theme.of(context).colorScheme.onSecondaryFixed, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + maxLines: 4, + controller: promptController, + enabled: !loading, + onTap: () { + promptController.selection = TextSelection( + baseOffset: 0, + extentOffset: promptController.text.length, + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: ElevatedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.primaryContainer, + ), + ), + onPressed: loading + ? null + : () => generateImages(context, promptController.text), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s24, + horizontal: 0, + ), + child: Column( + children: [ + const Icon(size: 32, Icons.brush), + const SizedBox.square(dimension: AppSpacing.s8), + const Text(textAlign: TextAlign.center, 'Create\nImage'), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/ui_components.dart new file mode 100644 index 0000000..759ffad --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/imagen/ui_components/ui_components.dart @@ -0,0 +1,16 @@ +// 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. + +export 'image_display.dart'; +export 'prompt_input.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart b/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart new file mode 100644 index 0000000..d167c70 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/firebaseai_live_api_service.dart @@ -0,0 +1,217 @@ +// 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:developer'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../shared/app_state.dart'; +import '../../shared/firebaseai_imagen_service.dart'; +import '../../shared/function_calling/tools.dart'; +import 'utilities/audio_output.dart'; + +/// A service that handles all communication with the Firebase AI Gemini Live API. +/// +/// This service demonstrates how to use the `liveGenerativeModel()` to create +/// a real-time, bidirectional audio & video stream with Gemini. It manages the +/// `LiveSession` and processes streaming responses, including tool calls. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/live-api?api=dev +class LiveApiService { + final AudioOutput _audioOutput; + final WidgetRef _ref; + // Callbacks for UI updates handled by setState + final void Function(bool isLoading) onImageLoadingChange; + final void Function(Uint8List imageBytes) onImageGenerated; + final void Function(String error) onError; + + LiveApiService({ + required AudioOutput audioOutput, + required WidgetRef ref, + required this.onImageLoadingChange, + required this.onImageGenerated, + required this.onError, + }) : _audioOutput = audioOutput, + _ref = ref; + + final LiveGenerativeModel + _liveModel = FirebaseAI.googleAI().liveGenerativeModel( + systemInstruction: Content.text( + 'You are a helpful assisant. If you have a tool to help the user, please use it.', + ), + model: 'gemini-live-2.5-flash-preview', + liveGenerationConfig: LiveGenerationConfig( + speechConfig: SpeechConfig(voiceName: 'fenrir'), + responseModalities: [ResponseModalities.audio], + ), + tools: [ + Tool.functionDeclarations([generateImageTool, setAppColorTool]), + ], + ); + + late LiveSession _session; + bool _liveSessionIsOpen = false; + + Future connect() async { + if (_liveSessionIsOpen) return; + try { + _session = await _liveModel.connect(); + _liveSessionIsOpen = true; + unawaited(processMessagesContinuously()); + } catch (e) { + log('Error connecting to live session: $e'); + onError('Failed to start the call. Please try again.'); + } + } + + Future close() async { + if (!_liveSessionIsOpen) return; + try { + await _session.close(); + } catch (e) { + log('Error closing live session: $e'); + // Don't necessarily need to show an error to the user on close. + } finally { + _liveSessionIsOpen = false; + } + } + + bool get isSessionOpen => _liveSessionIsOpen; + + void sendMediaStream(Stream stream) { + if (!_liveSessionIsOpen) return; + _session.sendMediaStream(stream); + } + + Future processMessagesContinuously() async { + try { + await for (final response in _session.receive()) { + LiveServerMessage message = response.message; + await _handleLiveServerMessage(message); + } + log('Live session receive stream completed.'); + } catch (e) { + log('Error receiving live session messages: $e'); + onError('Something went wrong during the call. Please try again.'); + } + } + + Future _handleLiveServerMessage(LiveServerMessage response) async { + if (response is LiveServerContent) { + if (response.modelTurn != null) { + await _handleLiveServerContent(response); + } + if (response.turnComplete != null && response.turnComplete!) { + await _handleTurnComplete(); + } + if (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) { + switch (part) { + case TextPart textPart: + await _handleTextPart(textPart); + case InlineDataPart inlineDataPart: + await _handleInlineDataPart(inlineDataPart); + default: + log('Received part with type ${part.runtimeType}'); + } + } + } + } + + Future _handleInlineDataPart(InlineDataPart part) async { + if (part.mimeType.startsWith('audio')) { + _audioOutput.addDataToAudioStream(part.bytes); + } + } + + Future _handleTextPart(TextPart part) async { + log('Text message from Gemini: ${part.text}'); + } + + Future _handleTurnComplete() async { + log('Model is done generating. Turn complete!'); + } + + Future _handleLiveServerToolCall(LiveServerToolCall response) async { + var functionCalls = response.functionCalls; + if (functionCalls == null || functionCalls.isEmpty) return; + + // The API currently only supports one function call per turn. + var functionCall = functionCalls.first; + log("Gemini made a function call: ${functionCall.name}"); + + switch (functionCall.name) { + case 'GenerateImage': + await _handleGenerateImage(functionCall); + break; + case 'SetAppColor': + _handleSetAppColor(functionCall); + break; + default: + log('Unknown function call: ${functionCall.name}'); + } + } + + Future _handleGenerateImage(FunctionCall functionCall) async { + onImageLoadingChange(true); + try { + final imageDescription = functionCall.args['description']?.toString(); + if (imageDescription == null) { + onError('Image generation failed: No description provided.'); + return; + } + final image = await ImagenService().generateImage(imageDescription); + onImageGenerated(image); + } catch (e) { + log('Error generating image: $e'); + onError('Sorry, the image could not be generated.'); + } finally { + onImageLoadingChange(false); + } + } + + void _handleSetAppColor(FunctionCall functionCall) { + try { + final red = functionCall.args['red']! as int; + final green = functionCall.args['green']! as int; + final blue = functionCall.args['blue']! as int; + final newSeedColor = Color.fromRGBO(red, green, blue, 1); + _ref.read(appStateProvider).setAppColor(newSeedColor); + } catch (e) { + log('Error setting app color from tool call: $e'); + onError('Sorry, there was an error applying the color.'); + } + } + + void dispose() { + if (_liveSessionIsOpen) { + unawaited(close()); + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart b/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart new file mode 100644 index 0000000..3fc5fac --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/live_api_demo.dart @@ -0,0 +1,283 @@ +// 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:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'ui_components/ui_components.dart'; +import 'utilities/utilities.dart'; +import 'firebaseai_live_api_service.dart'; + +class LiveAPIDemo extends ConsumerStatefulWidget { + const LiveAPIDemo({super.key}); + + @override + ConsumerState createState() => _LiveAPIDemoState(); +} + +/// The main state for the Live API demo. +/// +/// This stateful widget orchestrates the UI and manages the state for the demo, +/// including handling user input, managing the call lifecycle, and coordinating +/// with the [LiveApiService] and I/O utilities. +class _LiveAPIDemoState extends ConsumerState { + // Service for interacting with the Gemini API via Firebase AI. + late final LiveApiService _liveApiService; + + // Utilities for handling device I/O. + late final AudioInput _audioInput = AudioInput(); + late final AudioOutput _audioOutput = AudioOutput(); + late final VideoInput _videoInput = VideoInput(); + + // Initialization flags. + bool _audioIsInitialized = false; + bool _videoIsInitialized = false; + + // UI State flags. + bool _isConnecting = false; // True when setting up the Gemini session. + bool _isCallActive = false; // True when the audio stream is active. + bool _cameraIsActive = false; // True when sending video to Gemini. + bool _loadingImage = false; // True when waiting for an image to be generated. + + @override + void initState() { + super.initState(); + _liveApiService = LiveApiService( + audioOutput: _audioOutput, + ref: ref, // Pass the ref to the service + onImageLoadingChange: _onImageLoadingChange, + onImageGenerated: _onImageGenerated, + onError: _showErrorSnackBar, + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeAudio(); + _initializeVideo(); + }); + } + + @override + void dispose() { + _audioInput.dispose(); + _audioOutput.dispose(); + _videoInput.dispose(); + _liveApiService.dispose(); + super.dispose(); + } + + void _showErrorSnackBar(String message) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } + + //================================================================================ + // UI Callbacks + //================================================================================ + + void _onImageLoadingChange(bool isLoading) { + setState(() { + _loadingImage = isLoading; + }); + } + + void _onImageGenerated(Uint8List imageBytes) { + if (!mounted) return; + showDialog( + context: context, + builder: (context) { + return GeneratedImageDialog(imageBytes: imageBytes); + }, + ); + } + + //================================================================================ + // Call Lifecycle + //================================================================================ + + void toggleCall() async { + _isCallActive ? await stopCall() : await startCall(); + } + + Future startCall() async { + // Initialize the camera controller here to ensure it's fresh for each call. + // This prevents a bug where the camera preview freezes on subsequent calls. + if (_videoIsInitialized) { + await _videoInput.initializeCameraController(); + } + + setState(() { + _isConnecting = true; + }); + + await _liveApiService.connect(); + + setState(() { + _isConnecting = false; + }); + + var audioInputStream = await _audioInput.startRecordingStream(); + log('Audio input stream is recording!'); + + await _audioOutput.playStream(); + log('Audio output stream is playing!'); + + setState(() { + _isCallActive = true; + }); + + _liveApiService.sendMediaStream( + audioInputStream.map((data) { + return InlineDataPart('audio/pcm', data); + }), + ); + } + + Future stopCall() async { + if (_cameraIsActive) { + stopVideoStream(); + } + await _audioInput.stopRecording(); + await _audioOutput.stopStream(); + + setState(() { + _isConnecting = true; + }); + + await _liveApiService.close(); + + setState(() { + _isConnecting = false; + _isCallActive = false; + }); + } + + //================================================================================ + // I/O Initialization and Control + //================================================================================ + + Future _initializeAudio() async { + try { + await _audioInput.init(); // Initialize Audio Input + await _audioOutput.init(); // Initialize Audio Output + + setState(() { + _audioIsInitialized = true; + }); + } catch (e) { + log("Error during audio initialization: $e"); + if (!mounted) return; + + var errorSnackBar = SnackBar( + content: const Text('Oops! Something went wrong with the audio setup.'), + action: SnackBarAction(label: 'Retry', onPressed: _initializeAudio), + ); + ScaffoldMessenger.of(context).showSnackBar(errorSnackBar); + } + } + + Future _initializeVideo() async { + try { + await _videoInput.init(); + setState(() { + _videoIsInitialized = true; + }); + } catch (e) { + log("Error during video initialization: $e"); + } + } + + void startVideoStream() { + if (!_videoIsInitialized || !_isCallActive || _cameraIsActive) { + return; + } + + Stream imageStream = _videoInput.startStreamingImages(); + + _liveApiService.sendMediaStream( + imageStream.map((data) { + return InlineDataPart("image/jpeg", data); + }), + ); + + setState(() { + _cameraIsActive = true; + }); + } + + void stopVideoStream() async { + await _videoInput.stopStreamingImages(); + setState(() { + _cameraIsActive = false; + }); + } + + void toggleVideoStream() async { + _cameraIsActive ? stopVideoStream() : startVideoStream(); + } + + Future toggleMuteInput() async { + await _audioInput.togglePauseRecording(); + setState(() {}); // Rebuild mute button icon + } + + //================================================================================ + // Build Method + //================================================================================ + + @override + Widget build(BuildContext context) { + final audioInput = _audioInput; + final videoInput = _videoInput; + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: const LiveApiDemoAppBar(), + body: LiveApiBody( + cameraIsActive: _cameraIsActive, + cameraController: videoInput.controllerInitialized + ? videoInput.cameraController + : null, + settingUpLiveSession: _isConnecting, + loadingImage: _loadingImage, + ), + bottomNavigationBar: BottomBar( + children: [ + FlipCameraButton( + onPressed: _cameraIsActive && videoInput.cameras.length > 1 + ? videoInput.flipCamera + : null, + ), + VideoButton(isActive: _cameraIsActive, onPressed: toggleVideoStream), + AudioVisualizer( + audioStreamIsActive: _isCallActive, + amplitudeStream: audioInput.amplitudeStream, + ), + MuteButton( + isMuted: audioInput.isPaused, + onPressed: _isCallActive ? toggleMuteInput : null, + ), + CallButton( + isActive: _isCallActive, + onPressed: _audioIsInitialized ? toggleCall : null, + ), + ], + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart new file mode 100644 index 0000000..bbb1bd8 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/audio_visualizer.dart @@ -0,0 +1,40 @@ +// 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:waveform_flutter/waveform_flutter.dart'; +import 'sound_waves.dart'; + +class AudioVisualizer extends StatelessWidget { + const AudioVisualizer({ + super.key, + required this.audioStreamIsActive, + this.amplitudeStream, + }); + + final bool audioStreamIsActive; + final Stream? amplitudeStream; + + @override + Widget build(BuildContext context) { + return (audioStreamIsActive && amplitudeStream != null) + ? Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Soundwaves(amplitudeStream: amplitudeStream!), + ), + ) + : const Spacer(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart new file mode 100644 index 0000000..3d363ce --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/bottom_bar.dart @@ -0,0 +1,146 @@ +// 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 '../../../shared/ui/app_spacing.dart'; + +class BottomBar extends StatelessWidget { + const BottomBar({required this.children, super.key}); + + final List children; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: Row(children: children), + ), + ), + ); + } +} + +class FlipCameraButton extends StatelessWidget { + const FlipCameraButton({this.onPressed, super.key}); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + onPressed: onPressed, + icon: const Padding( + padding: EdgeInsets.all(AppSpacing.s4), + child: Icon(Icons.flip_camera_ios_outlined), + ), + ), + ); + } +} + +class VideoButton extends StatelessWidget { + const VideoButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: isActive + ? ButtonStyle( + backgroundColor: WidgetStateProperty.all( + const Color.fromARGB(240, 238, 255, 244), + ), + iconColor: WidgetStateProperty.all(Colors.black87), + ) + : const ButtonStyle(backgroundColor: null), + onPressed: onPressed, + icon: const Padding( + padding: EdgeInsets.all(AppSpacing.s4), + child: Icon(Icons.video_call_rounded), + ), + ), + ); + } +} + +class MuteButton extends StatelessWidget { + const MuteButton({required this.isMuted, this.onPressed, super.key}); + + final bool isMuted; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isMuted ? null : const Color.fromARGB(240, 238, 255, 244), + ), + ), + onPressed: onPressed, + icon: Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: isMuted + ? const Icon(Icons.mic_off) + : const Icon(color: Colors.black87, Icons.mic_none), + ), + ), + ); + } +} + +class CallButton extends StatelessWidget { + const CallButton({required this.isActive, this.onPressed, super.key}); + + final bool isActive; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: IconButton.filledTonal( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isActive + ? const Color.fromARGB(255, 199, 39, 27) + : Colors.green[500], + ), + ), + onPressed: onPressed, + icon: Padding( + padding: const EdgeInsets.all(AppSpacing.s4), + child: Icon(isActive ? Icons.phone_disabled_outlined : Icons.phone), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart new file mode 100644 index 0000000..9715c65 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/camera_previews.dart @@ -0,0 +1,91 @@ +// 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:camera/camera.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class SquareCameraPreview extends StatelessWidget { + const SquareCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 352.0, // Adjusted from 350 to be a multiple of 4 + height: 352.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(AppSpacing.s16), + ), + // The camera preview is often not a square. To fill the 1:1 aspect + // ratio, we scale the preview to cover the area and clip it. + child: Transform.scale( + scale: controller.value.aspectRatio / 1, + child: Center(child: CameraPreview(controller)), + ), + ), + ), + ), + ); + } +} + +class FullCameraPreview extends StatefulWidget { + const FullCameraPreview({required this.controller, super.key}); + + final CameraController controller; + + @override + State createState() => _FullCameraPreviewState(); +} + +class _FullCameraPreviewState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animController; + + @override + void initState() { + super.initState(); + _animController = AnimationController( + vsync: this, // the SingleTickerProviderStateMixin + duration: const Duration(seconds: 1), + ); + } + + @override + void dispose() { + super.dispose(); + _animController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s16), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(AppSpacing.s16)), + child: CameraPreview(widget.controller), + ), + ).animate(controller: _animController).scaleXY().fadeIn(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart new file mode 100644 index 0000000..9592813 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/generated_image_dialog.dart @@ -0,0 +1,52 @@ +// 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 'package:flutter/material.dart'; + +class GeneratedImageDialog extends StatelessWidget { + const GeneratedImageDialog({super.key, required this.imageBytes}); + + final Uint8List imageBytes; + + @override + Widget build(BuildContext context) { + return Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Center(child: Image.memory(imageBytes)), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: IconButton.filled( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart new file mode 100644 index 0000000..7e1dd58 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_api_body.dart @@ -0,0 +1,62 @@ +// 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:camera/camera.dart'; +import 'package:flutter/material.dart'; +import '../../../shared/ui/app_spacing.dart'; +import 'camera_previews.dart'; +import 'sound_waves.dart'; + +class LiveApiBody extends StatelessWidget { + const LiveApiBody({ + super.key, + required this.cameraIsActive, + this.cameraController, + required this.settingUpLiveSession, + required this.loadingImage, + }); + + final bool cameraIsActive; + final CameraController? cameraController; + final bool settingUpLiveSession; + final bool loadingImage; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: cameraIsActive && cameraController != null + ? Center(child: FullCameraPreview(controller: cameraController!)) + : CenterCircle( + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s60), + child: settingUpLiveSession + ? const CircularProgressIndicator() + : Image.asset('assets/gemini-logo.png'), + ), + ), + ), + if (loadingImage) + const Column( + children: [ + Text('Beep. Boop. Bop. Generating your image...'), + SizedBox.square(dimension: AppSpacing.s8), + LinearProgressIndicator(semanticsLabel: 'Generating image...'), + ], + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart new file mode 100644 index 0000000..7acd9d9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/live_demo_app_bar.dart @@ -0,0 +1,30 @@ +// 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'; + +class LiveApiDemoAppBar extends StatelessWidget implements PreferredSizeWidget { + const LiveApiDemoAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.transparent, + leadingWidth: 100, + title: const Text('Live API Demo'), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart new file mode 100644 index 0000000..a976380 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/sound_waves.dart @@ -0,0 +1,120 @@ +// 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:math'; +import 'package:flutter/material.dart'; +import 'package:waveform_flutter/waveform_flutter.dart'; +import '../../../shared/ui/app_spacing.dart'; + +class CenterCircle extends StatelessWidget { + const CenterCircle({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: CustomPaint( + size: const Size(160, 160), + painter: NestedCirclesPainter( + color: Theme.of(context).colorScheme.primary, + strokeWidth: 1.0, + gapBetweenCircles: 4.0, + ), + child: child, + ), + ); + } +} + +class Soundwaves extends StatelessWidget { + const Soundwaves({required this.amplitudeStream, super.key}); + + final Stream amplitudeStream; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: SizedBox( + height: 48, + child: AnimatedWaveList( + stream: amplitudeStream, + barBuilder: (animation, amplitude) { + return WaveFormBar( + amplitude: amplitude, + animation: animation, + color: Theme.of(context).colorScheme.primary, + ); + }, + ), + ), + ); + } +} + +// Custom Painter for drawing two nested circles +class NestedCirclesPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double gapBetweenCircles; // The space between the two circles + + NestedCirclesPainter({ + this.color = Colors.white54, // Default color for the circles + this.strokeWidth = 1.5, // Default stroke width for both circles + this.gapBetweenCircles = 4.0, // Default gap between the circles + }); + + @override + void paint(Canvas canvas, Size size) { + // Calculate the center of the drawing area + final Offset center = Offset(size.width / 2, size.height / 2); + + // Configure the paint properties (same for both circles) + final Paint paint = Paint() + ..color = color + .withValues(alpha: 0.7) // Make circles slightly transparent + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; // Draw the outline + + // Calculate the radius for the outer circle + // Ensure it fits within the bounds defined by 'size' + final double outerRadius = + min(size.width / 2, size.height / 2) - strokeWidth / 2; + + // Calculate the radius for the inner circle + final double innerRadius = + outerRadius - gapBetweenCircles - strokeWidth / 2; + + // Ensure inner radius is not negative + if (innerRadius > 0) { + // Draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + // Draw the inner circle + canvas.drawCircle(center, innerRadius, paint); + } else { + // If the gap is too large, just draw the outer circle + canvas.drawCircle(center, outerRadius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // Repaint only if properties change + return oldDelegate is NestedCirclesPainter && + (oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.gapBetweenCircles != gapBetweenCircles); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart new file mode 100644 index 0000000..d53ca4a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/ui_components/ui_components.dart @@ -0,0 +1,21 @@ +// 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. + +export 'bottom_bar.dart'; +export 'camera_previews.dart'; +export 'sound_waves.dart'; +export 'live_api_body.dart'; +export 'audio_visualizer.dart'; +export 'live_demo_app_bar.dart'; +export 'generated_image_dialog.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart new file mode 100644 index 0000000..bbdc482 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_input.dart @@ -0,0 +1,118 @@ +// 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:developer'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:record/record.dart'; +import 'package:waveform_flutter/waveform_flutter.dart' as wf; + +class AudioInput extends ChangeNotifier { + AudioRecorder? _recorder; + RecordConfig recordConfig = RecordConfig( + encoder: AudioEncoder.pcm16bits, + sampleRate: 24000, + numChannels: 1, + echoCancel: true, + noiseSuppress: true, + androidConfig: AndroidRecordConfig( + audioSource: AndroidAudioSource.voiceCommunication, + ), + iosConfig: IosRecordConfig(categoryOptions: []), + ); + bool isRecording = false; + bool isPaused = false; + late Stream audioStream; + Stream? amplitudeStream; + StreamSubscription? _amplitudeSubscription; + StreamController? _amplitudeStreamController; + + Future init() async { + _recorder = AudioRecorder(); + await checkPermission(); + } + + @override + void dispose() { + _recorder?.dispose(); + super.dispose(); + } + + Future checkPermission() async { + final hasPermission = await _recorder!.hasPermission(); + if (!hasPermission) { + throw MicrophonePermissionDeniedException( + 'This app does not have microphone permissions. Please enable it.', + ); + } + } + + Future> startRecordingStream() async { + final devices = await _recorder!.listInputDevices(); + log(devices.toString()); + audioStream = (await _recorder!.startStream( + recordConfig, + )).asBroadcastStream(); + _amplitudeStreamController = StreamController.broadcast(); + _amplitudeSubscription = _recorder! + .onAmplitudeChanged(const Duration(milliseconds: 100)) + .listen((amp) { + _amplitudeStreamController?.add( + wf.Amplitude(current: amp.current, max: amp.max), + ); + }); + amplitudeStream = _amplitudeStreamController?.stream; + isRecording = true; + //log("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + return audioStream; + } + + Future stopRecording() async { + await _recorder!.stop(); + isRecording = false; + await _amplitudeSubscription?.cancel(); + await _amplitudeStreamController?.close(); + amplitudeStream = null; + _recorder?.dispose(); + _recorder = AudioRecorder(); + //log("${isRecording ? "Is" : "Not"} Recording"); + notifyListeners(); + } + + Future togglePauseRecording() async { + isPaused ? await _recorder!.resume() : await _recorder!.pause(); + isPaused = !isPaused; + notifyListeners(); + return; + } +} + +/// 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'; + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart new file mode 100644 index 0000000..cce21ad --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/audio_output.dart @@ -0,0 +1,100 @@ +// 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:developer'; +import 'dart:typed_data'; +import 'package:flutter_soloud/flutter_soloud.dart'; + +class AudioOutput { + var initialized = false; + AudioSource? stream; + SoundHandle? handle; + final int sampleRate = 24000; + final Channels channels = Channels.mono; + final BufferType format = BufferType.s16le; // pcm16bits + + Future init() async { + if (initialized) { + return; + } + + /// Initialize the player (singleton). + await SoLoud.instance.init(sampleRate: sampleRate, channels: channels); + initialized = true; + } + + Future dispose() async { + if (initialized) { + SoLoud.instance.disposeAllSources(); + SoLoud.instance.deinit(); + initialized = false; + } + } + + SoLoud get instance => SoLoud.instance; + + AudioSource? setupNewStream() { + if (!SoLoud.instance.isInitialized) { + return null; + } + + stream = SoLoud.instance.setBufferStream( + maxBufferSizeBytes: + 1024 * 1024 * 10, // 10MB of max buffer (not allocated) + bufferingType: BufferingType.released, + bufferingTimeNeeds: 0, + sampleRate: sampleRate, + channels: channels, + format: format, + onBuffering: (isBuffering, handle, time) { + log('Buffering: $isBuffering, Time: $time'); + }, + ); + log("New audio output stream buffer created."); + return stream; + } + + Future playStream() async { + var myStream = setupNewStream(); + if (!SoLoud.instance.isInitialized || myStream == null) { + return null; + } + // Play audio stream + handle = await SoLoud.instance.play(myStream); + stream = myStream; + return stream; + } + + void addDataToAudioStream(Uint8List audioChunk) { + var currentStream = stream; + if (currentStream != null) { + SoLoud.instance.addAudioDataStream(currentStream, audioChunk); + } + } + + Future stopStream() async { + var currentStream = stream; + var currentHandle = handle; + + // Stream doesn't exist or handle is not valid - so nothing to stop. + if (currentStream == null || + currentHandle == null || + !SoLoud.instance.getIsValidVoiceHandle(currentHandle)) { + return; + } + // End data to stream & stop currently playing sound from handle + SoLoud.instance.setDataIsEnded(currentStream); + await SoLoud.instance.stop(currentHandle); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart new file mode 100644 index 0000000..a19db32 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/utilities.dart @@ -0,0 +1,17 @@ +// 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. + +export './audio_input.dart'; +export './audio_output.dart'; +export './video_input.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart new file mode 100644 index 0000000..07891a6 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/live_api/utilities/video_input.dart @@ -0,0 +1,135 @@ +// 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:developer'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class VideoInput extends ChangeNotifier { + late List _cameras; + CameraController? _cameraController; + CameraDescription? _selectedCamera; + bool controllerInitialized = false; + Timer? _captureTimer; + StreamController _imageStreamController = StreamController(); + bool _isStreaming = false; + + List get cameras => _cameras; + CameraController? get cameraController => _cameraController; + + Future init() async { + try { + _cameras = await availableCameras(); + if (_cameras.isNotEmpty) { + _selectedCamera = _cameras[0]; + } + } catch (e) { + log('Error getting available cameras: $e'); + } + } + + @override + void dispose() { + super.dispose(); + stopStreamingImages(); + if (controllerInitialized && _cameraController != null) { + _cameraController!.dispose(); + } + } + + Future initializeCameraController() async { + var cameraController = _cameraController; + if (controllerInitialized && cameraController != null) { + await cameraController.dispose(); + controllerInitialized = false; + } + + if (_selectedCamera == null) { + log("No camera selected or available."); + return; + } + + _cameraController = CameraController( + _selectedCamera!, + ResolutionPreset.veryHigh, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + try { + await _cameraController!.initialize(); + controllerInitialized = true; + notifyListeners(); + } catch (e) { + log('Error initializing camera: $e'); + } + } + + Stream startStreamingImages() { + if (_cameraController == null || !_cameraController!.value.isInitialized) { + throw ErrorSummary('Unable to start image stream'); + } + + _captureTimer = Timer.periodic( + const Duration(seconds: 1), // Capture images at 1 frame per second + (timer) async { + if (_cameraController == null || + !_cameraController!.value.isInitialized || + !_isStreaming) { + log("Stopping timer due to invalid state."); + stopStreamingImages(); + return; + } + + try { + // Prevent taking picture if already taking one + if (_cameraController!.value.isTakingPicture) { + return; + } + log("Taking picture..."); + final XFile imageFile = await _cameraController!.takePicture(); + Uint8List imageBytes = await imageFile.readAsBytes(); + _imageStreamController.add(imageBytes); + } catch (e) { + log('Error taking picture: $e'); + } + }, + ); + _isStreaming = true; + return _imageStreamController.stream; + } + + /// Stops the periodic image capture and closes the stream. + Future stopStreamingImages() async { + if (!_isStreaming) { + return; // Nothing to stop + } + _captureTimer?.cancel(); + await _imageStreamController.close(); + _imageStreamController = StreamController(); + _isStreaming = false; + } + + Future flipCamera() async { + if (_cameras.length > 1) { + final otherCamera = _cameras.firstWhere( + (camera) => camera.lensDirection != _selectedCamera?.lensDirection, + orElse: () => _cameras[0], + ); + _selectedCamera = otherCamera; + await initializeCameraController(); + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart new file mode 100644 index 0000000..39170e8 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/firebaseai_multimodal_service.dart @@ -0,0 +1,53 @@ +// 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:developer'; +import 'package:firebase_ai/firebase_ai.dart'; +import 'models/attachment.dart'; + +/// A service that handles all communication with the Firebase AI Gemini API +/// for the Multimodal demo. +/// +/// This service demonstrates how to use the `generateContent()` method on a +/// `GenerativeModel` to provide multimodal input, combining text and file +/// data (like images or PDFs) in a single prompt. +/// +/// For more informations, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/generate-text?api=dev#base64 +class MultimodalService { + final _model = FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.5-flash', + ); + + /// Generates text content from a text prompt and a file attachment. + /// + /// Throws an exception if the API call fails. + Future generateContent(String prompt, Attachment attachment) async { + try { + final attachmentPart = InlineDataPart( + attachment.mimeType, + attachment.fileBytes, + ); + + final response = await _model.generateContent([ + Content.multi([TextPart(prompt), attachmentPart]), + ]); + + return response.text; + } catch (e) { + log('Error generating content: $e'); + rethrow; + } + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart new file mode 100644 index 0000000..f1c2fd3 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/models/attachment.dart @@ -0,0 +1,27 @@ +// 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'; + +class Attachment { + String fileName; + String mimeType; + Uint8List fileBytes; + + Attachment({ + required this.fileName, + required this.mimeType, + required this.fileBytes, + }); +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart new file mode 100644 index 0000000..d1d95a9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/multimodal_demo.dart @@ -0,0 +1,125 @@ +// 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 'dart:developer' as dev; +import '../../shared/ui/app_frame.dart'; +import '../../shared/ui/app_spacing.dart'; +import './models/attachment.dart'; +import './firebaseai_multimodal_service.dart'; +import './ui_components/ui_components.dart'; +import 'utilities/file_picker_utility.dart'; + +class MultimodalDemo extends StatefulWidget { + const MultimodalDemo({super.key}); + + @override + State createState() => _MultimodalDemoState(); +} + +class _MultimodalDemoState extends State { + // Service for interacting with the Gemini API. + final _multimodalService = MultimodalService(); + + // UI State + bool _loading = false; + TextEditingController promptController = TextEditingController( + text: 'Please analyze this file and explain it to me like I\'m 5.', + ); + Attachment? _attachment; + ExpansibleController promptTileController = ExpansibleController(); + String? outputText; + + void _pickFile() async { + final newAttachment = await FilePickerService().pickFile(context); + if (newAttachment != null) { + setState(() { + _attachment = newAttachment; + }); + } + } + + void askGemini() async { + setState(() { + _loading = true; + }); + + var attachment = _attachment; + var prompt = promptController.text.trim(); + + if (attachment == null || prompt.isEmpty) { + setState(() { + _loading = false; + }); + return; + } + + promptTileController.collapse(); + + try { + outputText = await _multimodalService.generateContent(prompt, attachment); + } catch (e) { + dev.log(e.toString()); + outputText = 'Oops, sorry there was an error processing that file.'; + promptTileController.expand(); + } finally { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Multimodal Demo')), + body: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: AppFrame( + child: Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.s8, + 0, + AppSpacing.s8, + AppSpacing.s8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox.square(dimension: AppSpacing.s16), + FilePromptInput( + promptController: promptController, + tileController: promptTileController, + loading: _loading, + attachment: _attachment, + askGemini: askGemini, + onPickFilePressed: _pickFile, + onAttachmentChanged: (attachment) { + setState(() { + _attachment = attachment; + }); + }, + ), + OutputDisplay(loading: _loading, outputText: outputText), + ], + ), + ), + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart new file mode 100644 index 0000000..f4ac3d3 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/attachment_view.dart @@ -0,0 +1,80 @@ +// 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:thumbnailer/thumbnailer.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; + +class AttachmentView extends StatelessWidget { + final Attachment? attachment; + + const AttachmentView({super.key, this.attachment}); + + @override + Widget build(BuildContext context) { + return attachment == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 81, + color: Theme.of(context).colorScheme.outline, + Icons.attach_file, + ), + const SizedBox.square(dimension: AppSpacing.s16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: Text( + 'Select a file', + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Thumbnail( + decoration: WidgetDecoration( + wrapperSize: 90, + iconColor: Theme.of(context).colorScheme.primaryFixed, + ), + mimeType: attachment!.mimeType, + onlyIcon: true, + dataResolver: () async { + return attachment!.fileBytes; + }, + widgetSize: 200, + ), + const SizedBox.square(dimension: AppSpacing.s16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.s16), + child: Text( + attachment!.fileName, + style: TextStyle( + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart new file mode 100644 index 0000000..73b7ce9 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/dashed_border_painter.dart @@ -0,0 +1,69 @@ +// 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:math'; +import 'package:flutter/material.dart'; + +class DashedBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double dashWidth; + final double dashSpace; + final Radius radius; + + DashedBorderPainter({ + required this.color, + this.strokeWidth = 1.0, + this.dashWidth = 8.0, + this.dashSpace = 8.0, + this.radius = const Radius.circular(0), + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; + + final path = Path() + ..addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + radius, + ), + ); + + final dashPath = Path(); + double distance = 0.0; + + for (final pathMetric in path.computeMetrics()) { + while (distance < pathMetric.length) { + dashPath.addPath( + pathMetric.extractPath( + distance, + min(distance + dashWidth, pathMetric.length), + ), + Offset.zero, + ); + distance += dashWidth + dashSpace; + } + } + + canvas.drawPath(dashPath, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart new file mode 100644 index 0000000..0c9e99f --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/file_prompt_input.dart @@ -0,0 +1,178 @@ +// 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 '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; +import './attachment_view.dart'; +import './dashed_border_painter.dart'; + +class FilePromptInput extends StatelessWidget { + final TextEditingController promptController; + final ExpansibleController tileController; + final bool loading; + final Attachment? attachment; + final void Function() askGemini; + final void Function(Attachment?) onAttachmentChanged; + final VoidCallback onPickFilePressed; + + const FilePromptInput({ + super.key, + required this.promptController, + required this.tileController, + required this.loading, + required this.attachment, + required this.askGemini, + required this.onAttachmentChanged, + required this.onPickFilePressed, + }); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + controller: tileController, + collapsedShape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + title: Text( + style: Theme.of(context).textTheme.titleMedium, + 'File & Prompt', + ), + initiallyExpanded: true, + collapsedBackgroundColor: Theme.of(context).colorScheme.primaryContainer, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s16, + horizontal: AppSpacing.s24, + ), + child: Stack( + children: [ + Center( + child: GestureDetector( + onTap: onPickFilePressed, + child: CustomPaint( + painter: DashedBorderPainter( + color: Theme.of(context).colorScheme.outline, + strokeWidth: attachment == null ? 4 : 0, + radius: Radius.circular(AppSpacing.s16), + ), + child: Container( + width: 240, + height: 240, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + child: AttachmentView(attachment: attachment), + ), + ), + ), + ), + if (attachment != null) + Column( + children: [ + Center( + child: SizedBox( + width: 240, + height: 240, + child: Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: IconButton( + onPressed: () => onAttachmentChanged(null), + icon: Icon( + size: 32, + color: Theme.of(context).colorScheme.error, + Icons.close, + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox.square(dimension: AppSpacing.s24), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: TextField( + decoration: InputDecoration( + label: const Text('Prompt'), + fillColor: Theme.of(context).colorScheme.onSecondaryFixed, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + maxLines: 4, + controller: promptController, + enabled: !loading, + onTap: () { + promptController.selection = TextSelection( + baseOffset: 0, + extentOffset: promptController.text.length, + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: ElevatedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppSpacing.s16), + ), + ), + backgroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.primaryContainer, + ), + ), + onPressed: loading ? null : askGemini, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.s24, + horizontal: 0, + ), + child: Column( + children: [ + SizedBox( + width: 42, + height: 42, + child: Image.asset('assets/gemini-logo.png'), + ), + const SizedBox.square(dimension: AppSpacing.s4), + const Text(textAlign: TextAlign.center, 'Ask\nGemini'), + ], + ), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart new file mode 100644 index 0000000..0794462 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/output_display.dart @@ -0,0 +1,44 @@ +// 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'; +import '../../../shared/ui/app_spacing.dart'; + +class OutputDisplay extends StatelessWidget { + final bool loading; + final String? outputText; + + const OutputDisplay({super.key, required this.loading, this.outputText}); + + @override + Widget build(BuildContext context) { + return loading + ? Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: const LinearProgressIndicator(), + ) + : outputText != null + ? Padding( + padding: const EdgeInsets.fromLTRB( + AppSpacing.s24, + AppSpacing.s24, + AppSpacing.s24, + AppSpacing.s48, + ), + child: MarkdownBody(data: outputText!), + ) + : Container(); + } +} diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart new file mode 100644 index 0000000..0f14a00 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/ui_components/ui_components.dart @@ -0,0 +1,18 @@ +// 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. + +export 'attachment_view.dart'; +export 'dashed_border_painter.dart'; +export 'file_prompt_input.dart'; +export 'output_display.dart'; diff --git a/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart b/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart new file mode 100644 index 0000000..2395c8b --- /dev/null +++ b/firebase_ai_logic_showcase/lib/demos/multimodal/utilities/file_picker_utility.dart @@ -0,0 +1,158 @@ +// 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 'dart:developer'; +import 'package:cross_file/cross_file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mime/mime.dart'; +import '../../../shared/ui/app_spacing.dart'; +import '../models/attachment.dart'; + +const List imageFileTypes = ['png', 'jpeg', 'webp', 'jpg']; +const List videoFileTypes = [ + 'flv', + 'mov', + 'mpeg', + 'mpegps', + 'mpg', + 'mp4', + 'webm', + 'wmv', + '3gpp', +]; +const List audioFileTypes = [ + 'aac', + 'flac', + 'mp3', + 'm4a', + 'mpeg', + 'mpga', + 'mp4', + 'opus', + 'pcm', + 'wav', + 'webm', +]; +const List textFileTypes = ['pdf', 'txt']; + +class FilePickerService { + Future pickFile(BuildContext context) async { + final String? source = await _showFileSourcePicker(context); + if (source == null) return null; + + final FilePickerResult? result = await _pickFileFromSource(source); + if (result == null) return null; + + return _processFilePickerResult(result); + } + + Future _showFileSourcePicker(BuildContext context) async { + return await showModalBottomSheet( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, + context: context, + builder: (context) { + return Padding( + padding: const EdgeInsets.all(AppSpacing.s8), + child: SizedBox( + height: 240, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.s16, + vertical: AppSpacing.s8, + ), + child: Text( + style: Theme.of(context).textTheme.titleLarge, + 'File selection', + ), + ), + const Divider(), + if (!kIsWeb && Platform.isIOS) + ListTile( + leading: const Icon(size: 24, Icons.photo_library_rounded), + onTap: () { + Navigator.pop(context, 'Library'); + }, + title: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: Text( + style: Theme.of(context).textTheme.titleMedium, + 'Photos and videos', + ), + ), + ), + ListTile( + leading: const Icon(size: 24, Icons.folder), + onTap: () { + Navigator.pop(context, 'Files'); + }, + title: Padding( + padding: const EdgeInsets.only(left: AppSpacing.s8), + child: Text( + style: Theme.of(context).textTheme.titleMedium, + 'Browse files', + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Future _pickFileFromSource(String source) async { + if (source == 'Files') { + return await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: [ + ...audioFileTypes, + ...imageFileTypes, + ...videoFileTypes, + ...textFileTypes, + ], + ); + } else { + return await FilePicker.platform.pickFiles(type: FileType.media); + } + } + + Future _processFilePickerResult(FilePickerResult result) async { + var file = XFile(result.files.single.path!); + String fileName = result.files.single.name; + Uint8List fileBytes = await file.readAsBytes(); + String? mimeType = lookupMimeType( + fileName, + headerBytes: fileBytes.sublist(0, 10), + ); + + if (mimeType != null) { + return Attachment( + fileName: fileName, + mimeType: mimeType, + fileBytes: fileBytes, + ); + } else { + // Could not determine the file type. + log('Could not determine MIME type for ${file.path}'); + } + return null; + } +} diff --git a/firebase_ai_logic_showcase/lib/firebase_options.dart b/firebase_ai_logic_showcase/lib/firebase_options.dart new file mode 100644 index 0000000..a2ed568 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/firebase_options.dart @@ -0,0 +1,75 @@ +import 'dart:js_interop'; + +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show TargetPlatform, defaultTargetPlatform, kIsWeb; + +extension type BootstrapFirebaseOptions._(JSObject _) implements JSObject { + external String get apiKey; + external String get authDomain; + external String get databaseURL; + external String get projectId; + external String get storageBucket; + external String get messagingSenderId; + external String get appId; + external String get measurementId; +} + +extension type BootstrapOptions._(JSObject _) implements JSObject { + external String get geminiApiKey; + external BootstrapFirebaseOptions get firebase; +} + +@JS() +// ignore: non_constant_identifier_names +external BootstrapOptions get APP_TEMPLATE_BOOTSTRAP; + +class DefaultFirebaseOptions { + + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for android - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + // ignore: no_default_cases + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static FirebaseOptions web = FirebaseOptions( + apiKey: APP_TEMPLATE_BOOTSTRAP.firebase.apiKey, + appId: APP_TEMPLATE_BOOTSTRAP.firebase.appId, + messagingSenderId: APP_TEMPLATE_BOOTSTRAP.firebase.messagingSenderId, + projectId: APP_TEMPLATE_BOOTSTRAP.firebase.projectId, + authDomain: APP_TEMPLATE_BOOTSTRAP.firebase.authDomain, + storageBucket: APP_TEMPLATE_BOOTSTRAP.firebase.storageBucket, + ); +} diff --git a/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart b/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.dart new file mode 100644 index 0000000..659cb53 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/flutter_firebase_ai_demo.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 'shared/ui/app_frame.dart'; +import 'demos/chat/chat_demo.dart'; +import 'demos/multimodal/multimodal_demo.dart'; +import './demos/imagen/imagen_demo.dart'; +import './demos/live_api/live_api_demo.dart'; + +class Demo { + final String name; + final String description; + final IconData icon; + final Widget page; + + Demo({ + required this.name, + required this.description, + required this.icon, + required this.page, + }); +} + +List demos = [ + Demo( + name: 'Live API', + description: 'Real-time bidirectional audio & video streaming with Gemini.', + icon: Icons.video_call, + page: LiveAPIDemo(), + ), + Demo( + name: 'Imagen', + description: 'Generate images with a prompt.', + icon: Icons.format_paint, + page: ImagenDemo(), + ), + Demo( + name: 'Multimodal Prompt', + description: 'Ask Gemini about an image, audio, video, or PDF file.', + icon: Icons.attach_file, + page: MultimodalDemo(), + ), + Demo( + name: 'Chat with Gemini', + description: + 'Support for various models with tool calling and image generation.', + icon: Icons.chat, + page: ChatDemo(), + ), +]; + +class DemoHomeScreen extends StatelessWidget { + const DemoHomeScreen({super.key}); + + void showMoreInfo(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => SizedBox( + width: double.infinity, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text('Questions or Feedback?'), + actions: [ + IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(textAlign: TextAlign.center, 'Please let us know!'), + ], + ), + SelectableText( + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), + 'github.com/firebase/flutterfire/issues', + ), + SizedBox.square(dimension: 32), + Text( + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + 'Made with ❤️\nby the Flutter & Firebase AI Logic Teams', + ), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: Padding( + padding: EdgeInsets.fromLTRB(16, 8, 4, 8), + child: Image.asset('assets/firebase-ai-logic.png'), + ), + title: Text( + style: Theme.of(context).textTheme.titleLarge, + 'Flutter x Firebase AI Logic', + ), + actions: [ + Padding( + padding: EdgeInsets.fromLTRB(4, 8, 16, 8), + child: IconButton( + icon: Icon(Icons.info_outline), + onPressed: () => showMoreInfo(context), + ), + ), + ], + ), + body: AppFrame( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: Text( + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + "Build AI features for your Flutter apps using the Firebase AI Logic SDK.\nPowered by Google AI models.", + ), + ), + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(8), + itemBuilder: (context, index) { + final demo = demos[index]; + + return Padding( + padding: EdgeInsets.all(8), + child: ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => demo.page), + ), + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadiusGeometry.circular(16), + ), + leading: Icon(size: 32, demo.icon), + title: Text( + demo.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(demo.description), + tileColor: Theme.of(context).colorScheme.primaryContainer, + trailing: Icon( + Icons.arrow_forward, + color: Theme.of(context).colorScheme.primaryFixedDim, + ), + ), + ); + }, + itemCount: demos.length, + ), + ), + ], + ), + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/main.dart b/firebase_ai_logic_showcase/lib/main.dart new file mode 100644 index 0000000..9012964 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/main.dart @@ -0,0 +1,48 @@ +// 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_core/firebase_core.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'flutter_firebase_ai_demo.dart'; +import './firebase_options.dart'; +import 'shared/app_state.dart'; + +void main() async { + FirebaseOptions options = DefaultFirebaseOptions.currentPlatform; + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: options); + runApp(const ProviderScope(child: MyApp())); +} + +class MyApp extends ConsumerWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appColor = ref.watch(appStateProvider).appColor; + return MaterialApp( + title: 'Flutter x Firebase AI Logic Demo', + home: DemoHomeScreen(), + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: appColor, + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.tonalSpot, + ).copyWith(surface: appColor), + ), + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/app_state.dart b/firebase_ai_logic_showcase/lib/shared/app_state.dart new file mode 100644 index 0000000..a049d3a --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/app_state.dart @@ -0,0 +1,30 @@ +// 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_riverpod/flutter_riverpod.dart'; + +class AppState extends ChangeNotifier { + Color appColor = Color.fromARGB(255, 0, 32, 46); + + String setAppColor(Color color) { + appColor = color; + notifyListeners(); + return 'Color successfully changed to ${appColor.toString()}!'; + } +} + +final appStateProvider = ChangeNotifierProvider((ref) { + return AppState(); +}); diff --git a/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart b/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart new file mode 100644 index 0000000..e077944 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/firebaseai_imagen_service.dart @@ -0,0 +1,72 @@ +// 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:developer'; +import 'dart:typed_data'; +import 'package:firebase_ai/firebase_ai.dart'; + +/// A service that handles all communication with the Firebase AI Imagen API. +/// +/// This service demonstrates how to use the `imagenModel()` to generate images +/// from a text prompt. It showcases the text-to-image generation feature. +/// +/// For more information, see the official documentation: +/// https://firebase.google.com/docs/ai-logic/generate-images-imagen?api=dev +/// +/// This is a shared service, located in the /shared directory, because it is +/// used by multiple demos (Chat, Live API, and Imagen) to provide image +/// generation capabilities. +class ImagenService { + final _model = FirebaseAI.googleAI().imagenModel( + model: 'imagen-4.0-generate-001', + generationConfig: ImagenGenerationConfig(numberOfImages: 1), + ); + + /// Generates a single image from a text prompt. + /// + /// Throws an exception if the API call fails, allowing the UI to handle it. + Future generateImage(String prompt) async { + try { + final res = await _model.generateImages(prompt); + return res.images.first.bytesBase64Encoded; + } catch (e) { + log('Error generating image: $e'); + rethrow; + } + } + + /// Generates multiple images from a text prompt. + /// + /// Throws an exception if the API call fails, allowing the UI to handle it. + Future> generateImages( + String prompt, { + int numberOfImages = 4, + }) async { + try { + final model = FirebaseAI.googleAI().imagenModel( + model: 'imagen-4.0-generate-001', + generationConfig: ImagenGenerationConfig( + numberOfImages: numberOfImages, + ), + ); + final res = await model.generateImages(prompt); + return res.images + .map((ImagenInlineImage e) => e.bytesBase64Encoded) + .toList(); + } catch (e) { + log('Error generating images: $e'); + rethrow; + } + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart b/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart new file mode 100644 index 0000000..86f15ac --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/function_calling/tools.dart @@ -0,0 +1,47 @@ +// 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:firebase_ai/firebase_ai.dart'; + +final generateImageTool = FunctionDeclaration( + 'GenerateImage', + 'Generate an image by describing it.', + parameters: { + 'description': Schema.string( + description: + 'A description of the image that you want to generate. Be as specific as possible. More specific is better. Describe the contents of the image, the style, and any other specifics about the image that is to be generated.', + ), + }, +); + +final setAppColorTool = FunctionDeclaration( + 'SetAppColor', + 'Set the app color. You must pick a color that matches the hue that the user requests. When talking with the user, use a human-friendly description of the color instead of RGB values.', + parameters: { + 'red': Schema.integer( + description: + "The desired app color's RGB RED channel value that can range from 0 to 46", + ), + 'green': Schema.integer( + description: + "The desired app color's RGB GREEN channel value that can range from 0 to 46", + ), + 'blue': Schema.integer( + description: + "The desired app color's RGB BLUE channel that can range from 0 to 46", + ), + }, +); + +// See "live_api" and "chat" demos for full implementation of function calling. diff --git a/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart b/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart new file mode 100644 index 0000000..25772fe --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/app_frame.dart @@ -0,0 +1,31 @@ +// 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'; + +class AppFrame extends StatelessWidget { + const AppFrame({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints.loose(Size(900, double.infinity)), + child: child, + ), + ); + } +} diff --git a/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart b/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart new file mode 100644 index 0000000..8aeff26 --- /dev/null +++ b/firebase_ai_logic_showcase/lib/shared/ui/app_spacing.dart @@ -0,0 +1,35 @@ +// 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. + +/// A class to hold consistent spacing values for the application, +/// following a 4dp grid. +class AppSpacing { + /// 4.0 + static const double s4 = 4.0; + + /// 8.0 + static const double s8 = 8.0; + + /// 16.0 + static const double s16 = 16.0; + + /// 24.0 + static const double s24 = 24.0; + + /// 48.0 + static const double s48 = 48.0; + + /// 60.0 + static const double s60 = 60.0; +} diff --git a/firebase_ai_logic_showcase/pubspec.yaml b/firebase_ai_logic_showcase/pubspec.yaml new file mode 100644 index 0000000..8034f01 --- /dev/null +++ b/firebase_ai_logic_showcase/pubspec.yaml @@ -0,0 +1,39 @@ +name: flutter_firebase_ai_sample +description: "Flutter application demonstrates Firebase AI Logic capabilities through a series of interactive demos." +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + record: ^6.1.1 + flutter_riverpod: ^2.6.1 + flutter_soloud: ^3.1.6 + firebase_core: ^4.0.0 + camera: ^0.11.1 + flutter_image_compress: ^2.4.0 + waveform_flutter: ^1.2.0 + flutter_animate: ^4.5.2 + firebase_ai: ^3.1.0 + image_picker: ^1.1.2 + permission_handler: ^12.0.1 + flutter_markdown: ^0.7.7+1 + file_picker: ^10.2.1 + thumbnailer: ^3.1.0 + cross_file: ^0.3.4+2 + mime: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + assets: + - assets/gemini-logo.png + - assets/firebase-ai-logic.png diff --git a/firebase_ai_logic_showcase/web/404.html b/firebase_ai_logic_showcase/web/404.html new file mode 100644 index 0000000..829eda8 --- /dev/null +++ b/firebase_ai_logic_showcase/web/404.html @@ -0,0 +1,33 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + diff --git a/firebase_ai_logic_showcase/web/favicon.png b/firebase_ai_logic_showcase/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/firebase_ai_logic_showcase/web/favicon.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-192.png b/firebase_ai_logic_showcase/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-192.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-512.png b/firebase_ai_logic_showcase/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-512.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png b/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-maskable-192.png differ diff --git a/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png b/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/firebase_ai_logic_showcase/web/icons/Icon-maskable-512.png differ diff --git a/firebase_ai_logic_showcase/web/index.html b/firebase_ai_logic_showcase/web/index.html new file mode 100644 index 0000000..a7eed38 --- /dev/null +++ b/firebase_ai_logic_showcase/web/index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + flutter_firebase_ai_sample + + + + + + + + + + + + + \ No newline at end of file diff --git a/firebase_ai_logic_showcase/web/manifest.json b/firebase_ai_logic_showcase/web/manifest.json new file mode 100644 index 0000000..1edb532 --- /dev/null +++ b/firebase_ai_logic_showcase/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_firebase_ai_sample", + "short_name": "flutter_firebase_ai_sample", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file