├── .github ├── dependabot.yaml └── workflows │ ├── flutter.yml │ └── publish.yml ├── .gitignore ├── .idx └── dev.nix ├── .metadata ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README └── screenshot.png ├── analysis_options.yaml ├── example ├── .firebase │ └── hosting.YnVpbGQvd2Vi.cache ├── .gitignore ├── .metadata ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutter_ai_toolkit_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle.kts ├── assets │ ├── README.md │ ├── halloween-bg.png │ └── recipes_default.json ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── callbacks │ │ ├── on_cancel.dart │ │ └── on_error.dart │ ├── cupertino │ │ └── cupertino.dart │ ├── custom_styles │ │ └── custom_styles.dart │ ├── dark_mode │ │ └── dark_mode.dart │ ├── dark_style.dart │ ├── demo │ │ └── demo.dart │ ├── echo │ │ └── echo.dart │ ├── firebase_options.dart │ ├── function_calls │ │ └── function_calls.dart │ ├── gemini │ │ └── gemini.dart │ ├── history │ │ └── history.dart │ ├── logging │ │ └── logging.dart │ ├── main.dart │ ├── recipes │ │ ├── data │ │ │ ├── recipe_data.dart │ │ │ ├── recipe_repository.dart │ │ │ └── settings.dart │ │ ├── pages │ │ │ ├── edit_recipe_page.dart │ │ │ ├── home_page.dart │ │ │ └── split_or_tabs.dart │ │ ├── recipes.dart │ │ └── views │ │ │ ├── recipe_content_view.dart │ │ │ ├── recipe_list_view.dart │ │ │ ├── recipe_response_view.dart │ │ │ ├── recipe_view.dart │ │ │ ├── search_box.dart │ │ │ └── settings_drawer.dart │ ├── restricted │ │ └── restricted.dart │ ├── styles │ │ └── styles.dart │ ├── suggestions │ │ └── suggestions.dart │ ├── vertex │ │ └── vertex.dart │ └── welcome │ │ └── welcome.dart ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ │ └── MainMenu.xib │ │ ├── Configs │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ └── RunnerTests │ │ └── RunnerTests.swift ├── pubspec.yaml └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json ├── font_svg ├── README.md ├── reply_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.svg ├── spark-icon.svg └── submit-icon.svg ├── idx-template.json ├── idx-template.nix ├── lib ├── flutter_ai_toolkit.dart ├── fonts │ └── FatIcons.ttf └── src │ ├── chat_view_model │ ├── chat_view_model.dart │ ├── chat_view_model_client.dart │ └── chat_view_model_provider.dart │ ├── dialogs │ ├── adaptive_dialog.dart │ ├── adaptive_snack_bar │ │ ├── adaptive_snack_bar.dart │ │ └── cupertino_snack_bar.dart │ └── image_preview_dialog.dart │ ├── llm_exception.dart │ ├── platform_helper │ ├── platform_helper.dart │ ├── platform_helper_io.dart │ └── platform_helper_web.dart │ ├── providers │ ├── implementations │ │ ├── echo_provider.dart │ │ └── firebase_provider.dart │ ├── interface │ │ ├── attachments.dart │ │ ├── chat_message.dart │ │ ├── llm_provider.dart │ │ └── message_origin.dart │ └── providers.dart │ ├── styles │ ├── action_button_style.dart │ ├── action_button_type.dart │ ├── chat_input_style.dart │ ├── file_attachment_style.dart │ ├── llm_chat_view_style.dart │ ├── llm_message_style.dart │ ├── styles.dart │ ├── suggestion_style.dart │ ├── tookit_icons.dart │ ├── toolkit_colors.dart │ ├── toolkit_text_styles.dart │ └── user_message_style.dart │ ├── utility.dart │ └── views │ ├── action_button.dart │ ├── adaptive_progress_indicator.dart │ ├── attachment_view │ ├── attachment_view.dart │ ├── file_attatchment_view.dart │ └── image_attachment_view.dart │ ├── chat_history_view.dart │ ├── chat_input │ ├── attachments_action_bar.dart │ ├── attachments_view.dart │ ├── chat_input.dart │ ├── chat_suggestion_view.dart │ ├── editing_indicator.dart │ ├── input_button.dart │ ├── input_state.dart │ ├── removable_attachment.dart │ └── text_or_audio_input.dart │ ├── chat_message_view │ ├── adaptive_copy_text.dart │ ├── hovering_buttons.dart │ ├── llm_message_view.dart │ └── user_message_view.dart │ ├── chat_text_field.dart │ ├── jumping_dots_progress_indicator │ ├── jumping_dot.dart │ └── jumping_dots_progress_indicator.dart │ ├── llm_chat_view │ ├── llm_chat_view.dart │ └── llm_response.dart │ └── response_builder.dart ├── pubspec.yaml └── test ├── smoke_test.dart └── suggestions_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | # Github actions ecosystem. 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | # Updating patch versions for "github-actions" is too chatty. 10 | # See https://github.com/flutter/flutter/issues/158350. 11 | ignore: 12 | - dependency-name: "*" 13 | update-types: ["version-update:semver-patch"] 14 | # Pub ecosystem. 15 | - package-ecosystem: "pub" 16 | versioning-strategy: "increase-if-necessary" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" 20 | -------------------------------------------------------------------------------- /.github/workflows/flutter.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | schedule: 10 | - cron: "0 0 * * *" # Every day at midnight 11 | 12 | jobs: 13 | lint-and-test: 14 | name: Test Flutter ${{ matrix.flutter_version }} 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | flutter_version: [stable, beta] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Flutter 23 | uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 24 | with: 25 | channel: ${{ matrix.flutter_version }} 26 | - run: dart --version 27 | - run: flutter --version 28 | - run: flutter pub get 29 | - name: Lint analysis 30 | run: dart analyze --fatal-infos 31 | - name: Dart format 32 | run: dart format --output none --set-exit-if-changed . 33 | - name: dart fix 34 | run: dart fix --dry-run 35 | - name: Run tests 36 | run: flutter test 37 | - name: Check API docs can generate 38 | run: dart doc --dry-run 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | publish: 10 | name: Publish to pub.dev 11 | runs-on: ubuntu-latest 12 | permissions: 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: dart-lang/setup-dart@v1 17 | - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 18 | with: 19 | channel: 'stable' 20 | - run: flutter pub get 21 | - name: Publish 22 | run: flutter pub publish --force 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | build/ 30 | example/.env 31 | example/firebase.json 32 | example/.firebaserc 33 | example/android/app/google-services.json 34 | example/ios/Runner/GoogleService-Info.plist 35 | example/macos/Runner/GoogleService-Info.plist 36 | example/lib/echo.dart 37 | .flutter-plugins 38 | .flutter-plugins-dependencies 39 | .vscode/settings.json 40 | -------------------------------------------------------------------------------- /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | # To learn more about how to use Nix to configure your environment 2 | # see: https://developers.google.com/idx/guides/customize-idx-env 3 | { pkgs, ... }: { 4 | # Which nixpkgs channel to use. 5 | channel = "stable-24.05"; # or "unstable" 6 | # Use https://search.nixos.org/packages to find packages 7 | packages = [ 8 | pkgs.nodePackages.firebase-tools 9 | pkgs.jdk17 10 | pkgs.unzip 11 | ]; 12 | # Sets environment variables in the workspace 13 | env = { 14 | PATH = ["/home/user/.pub-cache/bin" "/home/user/flutter/bin" "./.flutter-sdk/flutter/bin"]; 15 | }; 16 | idx = { 17 | # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" 18 | extensions = [ 19 | "Dart-Code.flutter" 20 | "Dart-Code.dart-code" 21 | ]; 22 | workspace = { 23 | # Runs when a workspace is first created with this `dev.nix` file 24 | onCreate = { 25 | build-flutter = '' 26 | cd /home/user/myapp/example/android 27 | 28 | ./gradlew \ 29 | --parallel \ 30 | -Pverbose=true \ 31 | -Ptarget-platform=android-x86 \ 32 | -Ptarget=/home/user/myapp/lib/main.dart \ 33 | -Pbase-application-name=android.app.Application \ 34 | -Pdart-defines=RkxVVFRFUl9XRUJfQ0FOVkFTS0lUX1VSTD1odHRwczovL3d3dy5nc3RhdGljLmNvbS9mbHV0dGVyLWNhbnZhc2tpdC85NzU1MDkwN2I3MGY0ZjNiMzI4YjZjMTYwMGRmMjFmYWMxYTE4ODlhLw== \ 35 | -Pdart-obfuscation=false \ 36 | -Ptrack-widget-creation=true \ 37 | -Ptree-shake-icons=false \ 38 | -Pfilesystem-scheme=org-dartlang-root \ 39 | assembleDebug 40 | 41 | # TODO: Execute web build in debug mode. 42 | # flutter run does this transparently either way 43 | # https://github.com/flutter/flutter/issues/96283#issuecomment-1144750411 44 | # flutter build web --profile --dart-define=Dart2jsOptimization=O0 45 | 46 | adb -s localhost:5555 wait-for-device 47 | ''; 48 | installDependencies = "flutter channel stable && flutter upgrade && flutter pub get"; 49 | }; 50 | }; 51 | # Enable previews and customize configuration 52 | previews = { 53 | enable = true; 54 | previews = { 55 | web = { 56 | command = ["flutter" "run" "--machine" "-d" "web-server" "--web-hostname" "0.0.0.0" "--web-port" "$PORT"]; 57 | manager = "flutter"; 58 | cwd = "example"; 59 | }; 60 | android = { 61 | command = ["flutter" "run" "--machine" "-d" "android" "-d" "localhost:5555"]; 62 | manager = "flutter"; 63 | cwd = "example"; 64 | }; 65 | }; 66 | }; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "main", 6 | "cwd": "example", 7 | "request": "launch", 8 | "type": "dart", 9 | "program": "lib/main.dart", 10 | }, 11 | { 12 | "name": "gemini", 13 | "cwd": "example", 14 | "request": "launch", 15 | "type": "dart", 16 | "program": "lib/gemini/gemini.dart", 17 | }, 18 | { 19 | "name": "vertex", 20 | "cwd": "example", 21 | "request": "launch", 22 | "type": "dart", 23 | "program": "lib/vertex/vertex.dart", 24 | }, 25 | { 26 | "name": "demo", 27 | "cwd": "example", 28 | "request": "launch", 29 | "type": "dart", 30 | "program": "lib/demo/demo.dart", 31 | }, 32 | { 33 | "name": "welcome", 34 | "cwd": "example", 35 | "request": "launch", 36 | "type": "dart", 37 | "program": "lib/welcome/welcome.dart", 38 | }, 39 | { 40 | "name": "cupertino", 41 | "cwd": "example", 42 | "request": "launch", 43 | "type": "dart", 44 | "program": "lib/cupertino/cupertino.dart", 45 | }, 46 | { 47 | "name": "custom styles", 48 | "cwd": "example", 49 | "request": "launch", 50 | "type": "dart", 51 | "program": "lib/custom_styles/custom_styles.dart", 52 | }, 53 | { 54 | "name": "dark mode", 55 | "cwd": "example", 56 | "request": "launch", 57 | "type": "dart", 58 | "program": "lib/dark_mode/dark_mode.dart", 59 | }, 60 | { 61 | "name": "history", 62 | "cwd": "example", 63 | "request": "launch", 64 | "type": "dart", 65 | "program": "lib/history/history.dart", 66 | }, 67 | { 68 | "name": "suggestions", 69 | "cwd": "example", 70 | "request": "launch", 71 | "type": "dart", 72 | "program": "lib/suggestions/suggestions.dart", 73 | }, 74 | { 75 | "name": "logging", 76 | "cwd": "example", 77 | "request": "launch", 78 | "type": "dart", 79 | "program": "lib/logging/logging.dart", 80 | }, 81 | { 82 | "name": "on cancel", 83 | "cwd": "example", 84 | "request": "launch", 85 | "type": "dart", 86 | "program": "lib/callbacks/on_cancel.dart", 87 | }, 88 | { 89 | "name": "on error", 90 | "cwd": "example", 91 | "request": "launch", 92 | "type": "dart", 93 | "program": "lib/callbacks/on_error.dart", 94 | }, 95 | { 96 | "name": "function calls", 97 | "cwd": "example", 98 | "request": "launch", 99 | "type": "dart", 100 | "program": "lib/function_calls/function_calls.dart", 101 | }, 102 | { 103 | "name": "restricted", 104 | "cwd": "example", 105 | "request": "launch", 106 | "type": "dart", 107 | "program": "lib/restricted/restricted.dart", 108 | }, 109 | { 110 | "name": "recipes", 111 | "cwd": "example", 112 | "request": "launch", 113 | "type": "dart", 114 | "program": "lib/recipes/recipes.dart", 115 | }, 116 | ] 117 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 The Flutter Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/README/screenshot.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: true 6 | -------------------------------------------------------------------------------- /example/.firebase/hosting.YnVpbGQvd2Vi.cache: -------------------------------------------------------------------------------- 1 | manifest.json,1722130836723,f81e4554dc7f05633a2c5597416813859de5ace688342db41b201d42790fb8a7 2 | flutter.js,1726080818000,dec659847e4e16b505a257eb15bc32ef814f99a319e44e15b9c56294de0c9ecd 3 | favicon.png,1716580864393,fcc7c4545d5b62ad01682589e6fdc7ea03d0a3b42069963c815c344b632eb5cf 4 | icons/Icon-maskable-512.png,1716580886213,e7983524dc70254adc61764657d7e03d19284de8da586b5818d737bc08c6d14e 5 | icons/Icon-maskable-192.png,1716580886213,dd96c123fdf6817cdf7e63d9693bcc246bac2e3782a41a6952fa41c0617c5573 6 | icons/Icon-512.png,1716580864394,7a31ce91e554f1941158ca46f31c7f3f2b7c8c129229ea74a8fae1affe335033 7 | icons/Icon-192.png,1716580864394,d2e0131bb7851eb9d98f7885edb5ae4b4d6b7a6c7addf8a25b9b712b39274c0f 8 | canvaskit/skwasm.worker.js,1726081188000,ff1a6b2c254c1954106d62ce10432547fa6d6a7d99c0aaf9ea6144a23ef0c09b 9 | canvaskit/skwasm.wasm,1726081188000,204bb6c7deaa41ccfdb811d09107bcacd7162a848d8e9faa45e46390fe0dda9a 10 | canvaskit/skwasm.js.symbols,1726081188000,ea615fdcd1320bad0f3fb6b3e5bcb0dd3a4d8480c221af6a84455860775987c1 11 | canvaskit/skwasm.js,1726081188000,3da3d8d5b168f8f2cba2513cba2c65a9bfa650723545145cffa76a102a678e9d 12 | canvaskit/canvaskit.wasm,1726081100000,2e91b313aa59675be755df469bb88efdfa2be8cdccc4e49bf743f9f22d16f933 13 | canvaskit/canvaskit.js.symbols,1726081098000,7687fed87ac2c6f61e5d991be1e3f8c0191e4271128c73a5afea2f28562ad0b8 14 | canvaskit/canvaskit.js,1726081100000,95f051f3c2845fb04127fd56e1fa3e52a69d271eea561869e97bfe4374548109 15 | canvaskit/chromium/canvaskit.wasm,1726081122000,d865adf21902388e4d4af54a5e430479e5ef37ac660649017db1877b29976a08 16 | canvaskit/chromium/canvaskit.js.symbols,1726081120000,9d7b8e9cc146e9ad2b68af9f0c8092b144a9aac73666941b468b8b2fd36cdb27 17 | canvaskit/chromium/canvaskit.js,1726081122000,c448a9b3e29d0dad724aa33a554695e7f1257c58b28baca305177fd18e62e411 18 | assets/packages/record_web/assets/js/record.worklet.js,1726247519128,bd510fc16afe17c0cfc943194267c8d2af8a4b88fa63b525926c95d319e544c7 19 | assets/packages/record_web/assets/js/record.fixwebmduration.js,1726247519130,77e2fe77324499420d0a882273998e7574fa885c3a7d944a8e69382e5daaab08 20 | assets/assets/recipes_default.json,1727751782273,1f90b422a85d1e7a708fd4d255d8b7db1b6206bf002813cb380d6cce0fbb4bd8 21 | assets/assets/halloween-bg.png,1729875831180,aa155fdcd4249179b89ca8e6dbfbff642a622534635e9cc6dce5c099d7445b7e 22 | version.json,1730136001205,8e7012f17ec662cfb721c69523e82a27ac216e8e3c973b248b847e9a118cf603 23 | index.html,1730135983527,4c1651c60b2cd671eb40ebf9e77cd7d7f1894a90b21182fa149ead727d85095f 24 | assets/FontManifest.json,1730136001277,d1784484a5fbf123cc38d6fb83f618e2f855cfc9a47c4b87c34565736fb1025f 25 | flutter_service_worker.js,1730136002322,09a340c85d8522734c4db9521a920665aef469eec44a7162064304be50c817ee 26 | assets/AssetManifest.json,1730136001277,5ee990889da9f2ce04fb308d72dfb0a4df0a2a8c4cfe808dcc784e0cea9560ea 27 | assets/AssetManifest.bin.json,1730136001277,599a0b1e65a657294c65f94df02a01c115e4f2ef9a651ce3bbadad941866dc9d 28 | assets/AssetManifest.bin,1730136001277,71fc78fcaf72348505ddacbfc2fc03fb686e543376e18388ecdca2509e70245d 29 | flutter_bootstrap.js,1730135983509,257003d38cfc778893260c7ce9a69c191aa5c23c05402fb287677767cc049188 30 | assets/packages/flutter_ai_toolkit/lib/fonts/FatIcons.ttf,1730136002032,59c80e8640bde5d1fc73af52961bb52ed3f08fddd3af8f8fe172880442cc33b7 31 | assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1730136002030,a9dec9e47fcee105fc5f7ea79904e588215596ef681f1ba97034cd0829c0554b 32 | assets/shaders/ink_sparkle.frag,1730136001332,80c6e65c75f1de434b1b22dba61e96ad82dba0f2fc5e8b3b59c2def46d794354 33 | assets/fonts/MaterialIcons-Regular.otf,1730136002034,d1e5ecfde56f17e87cc9b0e73792a40eadf95ed0372e336e13038744c8dc09cf 34 | assets/NOTICES,1730136001278,b7c968ec8b44aa16ccd1a0138cfee73251738e425cae9fcc8fbdca9051337fc6 35 | main.dart.js,1730136001025,34484a94aff99c34bb6608f95d1a7e3d39b2bd66184d520341ff826ace776120 36 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | pubspec.lock 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 17 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 18 | - platform: android 19 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 20 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 21 | - platform: ios 22 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 23 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 24 | - platform: macos 25 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 26 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 27 | - platform: web 28 | create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 29 | base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 30 | 31 | # User provided section 32 | 33 | # List of Local paths (relative to this file) that should be 34 | # ignored by the migrate tool. 35 | # 36 | # Files that are not part of the templates will be ignored by default. 37 | unmanaged_files: 38 | - 'lib/main.dart' 39 | - 'ios/Runner.xcodeproj/project.pbxproj' 40 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /example/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | // START: FlutterFire Configuration 4 | id("com.google.gms.google-services") 5 | // END: FlutterFire Configuration 6 | id("kotlin-android") 7 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 8 | id("dev.flutter.flutter-gradle-plugin") 9 | } 10 | 11 | android { 12 | namespace = "com.example.flutter_ai_toolkit_example" 13 | compileSdk = flutter.compileSdkVersion 14 | ndkVersion = flutter.ndkVersion 15 | 16 | compileOptions { 17 | sourceCompatibility = JavaVersion.VERSION_11 18 | targetCompatibility = JavaVersion.VERSION_11 19 | } 20 | 21 | kotlinOptions { 22 | jvmTarget = JavaVersion.VERSION_11.toString() 23 | } 24 | 25 | defaultConfig { 26 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 27 | applicationId = "com.example.flutter_ai_toolkit_example" 28 | // You can update the following values to match your application needs. 29 | // For more information, see: https://flutter.dev/to/review-gradle-config. 30 | minSdk = 23 // Firebase requires Android 23 31 | targetSdk = flutter.targetSdkVersion 32 | versionCode = flutter.versionCode 33 | versionName = flutter.versionName 34 | } 35 | 36 | buildTypes { 37 | release { 38 | // TODO: Add your own signing config for the release build. 39 | // Signing with the debug keys for now, so `flutter run --release` works. 40 | signingConfig = signingConfigs.getByName("debug") 41 | } 42 | } 43 | } 44 | 45 | flutter { 46 | source = "../.." 47 | } 48 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/flutter_ai_toolkit_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_ai_toolkit_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | // START: FlutterFire Configuration 23 | id("com.google.gms.google-services") version("4.3.15") apply false 24 | // END: FlutterFire Configuration 25 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false 26 | } 27 | 28 | include(":app") 29 | -------------------------------------------------------------------------------- /example/assets/README.md: -------------------------------------------------------------------------------- 1 | The following assets have been generated by the Gemini LLM and are being used royalty-free in this project: 2 | - halloween-bg.png 3 | - recipes_default.json -------------------------------------------------------------------------------- /example/assets/halloween-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/assets/halloween-bg.png -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Firebase requires iOS 13 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Ai Toolkit Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_ai_toolkit_example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSCameraUsageDescription 28 | $(PRODUCT_NAME) would like to access your camera. 29 | NSMicrophoneUsageDescription 30 | $(PRODUCT_NAME) would like to access your microphone. 31 | NSPhotoLibraryUsageDescription 32 | $(PRODUCT_NAME) would like access to your photos. 33 | UIApplicationSupportsIndirectInputEvents 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | CADisableMinimumFrameDurationOnPhone 53 | 54 | UIApplicationSupportsIndirectInputEvents 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/lib/callbacks/on_cancel.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: On Cancel'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | void _onCancel(BuildContext context) { 33 | ScaffoldMessenger.of( 34 | context, 35 | ).showSnackBar(const SnackBar(content: Text('Chat cancelled'))); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) => Scaffold( 40 | appBar: AppBar(title: const Text(App.title)), 41 | body: LlmChatView( 42 | onCancelCallback: _onCancel, 43 | cancelMessage: 'Request cancelled', 44 | provider: FirebaseProvider( 45 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 46 | ), 47 | ), 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /example/lib/callbacks/on_error.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: On Error'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | void _onError(BuildContext context, LlmException error) { 33 | ScaffoldMessenger.of( 34 | context, 35 | ).showSnackBar(SnackBar(content: Text('Error: ${error.message}'))); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) => Scaffold( 40 | appBar: AppBar(title: const Text(App.title)), 41 | body: LlmChatView( 42 | onErrorCallback: _onError, 43 | errorMessage: 'An error occurred', 44 | provider: FirebaseProvider( 45 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 46 | ), 47 | ), 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /example/lib/cupertino/cupertino.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Cupertino'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const CupertinoApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | @override 33 | Widget build(BuildContext context) => CupertinoPageScaffold( 34 | navigationBar: CupertinoNavigationBar(middle: Text(App.title)), 35 | child: LlmChatView( 36 | provider: FirebaseProvider( 37 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /example/lib/dark_mode/dark_mode.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | import '../dark_style.dart'; 11 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 12 | import '../firebase_options.dart'; 13 | 14 | void main() async { 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 17 | runApp(const App()); 18 | } 19 | 20 | class App extends StatelessWidget { 21 | static const title = 'Example: Dark Mode'; 22 | static final themeMode = ValueNotifier(ThemeMode.dark); 23 | 24 | const App({super.key}); 25 | 26 | @override 27 | Widget build(BuildContext context) => ValueListenableBuilder( 28 | valueListenable: themeMode, 29 | builder: 30 | (BuildContext context, ThemeMode mode, Widget? child) => MaterialApp( 31 | title: title, 32 | theme: ThemeData.light(), 33 | darkTheme: ThemeData.dark(), 34 | themeMode: mode, 35 | home: ChatPage(), 36 | debugShowCheckedModeBanner: false, 37 | ), 38 | ); 39 | } 40 | 41 | class ChatPage extends StatefulWidget { 42 | const ChatPage({super.key}); 43 | 44 | @override 45 | State createState() => _ChatPageState(); 46 | } 47 | 48 | class _ChatPageState extends State { 49 | final _provider = FirebaseProvider( 50 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 51 | ); 52 | 53 | final _lightStyle = LlmChatViewStyle.defaultStyle(); 54 | final _darkStyle = darkChatViewStyle(); 55 | 56 | @override 57 | Widget build(BuildContext context) => Scaffold( 58 | appBar: AppBar( 59 | title: const Text(App.title), 60 | actions: [ 61 | IconButton( 62 | onPressed: 63 | () => 64 | App.themeMode.value = 65 | App.themeMode.value == ThemeMode.light 66 | ? ThemeMode.dark 67 | : ThemeMode.light, 68 | tooltip: 69 | App.themeMode.value == ThemeMode.light 70 | ? 'Dark Mode' 71 | : 'Light Mode', 72 | icon: const Icon(Icons.brightness_4_outlined), 73 | ), 74 | ], 75 | ), 76 | body: LlmChatView( 77 | provider: _provider, 78 | style: App.themeMode.value == ThemeMode.dark ? _darkStyle : _lightStyle, 79 | ), 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /example/lib/echo/echo.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | 8 | void main() => runApp(App()); 9 | 10 | class App extends StatefulWidget { 11 | static const title = 'Example: Echo Test'; 12 | 13 | const App({super.key}); 14 | 15 | @override 16 | State createState() => _AppState(); 17 | } 18 | 19 | class _AppState extends State { 20 | final _provider = EchoProvider(); 21 | 22 | @override 23 | Widget build(BuildContext context) => MaterialApp( 24 | title: App.title, 25 | home: Scaffold( 26 | appBar: AppBar(title: const Text(App.title)), 27 | body: LlmChatView(provider: _provider), 28 | ), 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File normally generated by FlutterFire CLI. This is a stand-in. 2 | // See README.md for details. 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | 5 | class DefaultFirebaseOptions { 6 | // TODO: Remove this file and run `flutterfire config` for Firebase options 7 | static FirebaseOptions get currentPlatform { 8 | throw UnimplementedError( 9 | 'Generate this file by running `flutterfire configure`. ' 10 | 'See README.md for details.', 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/lib/function_calls/function_calls.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Function Calls'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | @override 33 | Widget build(BuildContext context) => Scaffold( 34 | appBar: AppBar(title: const Text(App.title)), 35 | body: LlmChatView( 36 | provider: FirebaseProvider( 37 | model: FirebaseAI.googleAI().generativeModel( 38 | model: 'gemini-2.0-flash', 39 | tools: [ 40 | Tool.functionDeclarations([ 41 | FunctionDeclaration( 42 | 'get_temperature', 43 | 'Get the current local temperature', 44 | parameters: {}, 45 | ), 46 | FunctionDeclaration( 47 | 'get_time', 48 | 'Get the current local time', 49 | parameters: {}, 50 | ), 51 | ]), 52 | ], 53 | ), 54 | onFunctionCall: _onFunctionCall, 55 | ), 56 | ), 57 | ); 58 | 59 | // note: we're not actually calling any external APIs in this example 60 | Future?> _onFunctionCall( 61 | FunctionCall functionCall, 62 | ) async => switch (functionCall.name) { 63 | 'get_temperature' => {'temperature': 60, 'unit': 'F'}, 64 | 'get_time' => {'time': DateTime(1970, 1, 1).toIso8601String()}, 65 | _ => throw Exception('Unknown function call: ${functionCall.name}'), 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /example/lib/gemini/gemini.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Google Gemini AI'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | @override 33 | Widget build(BuildContext context) => Scaffold( 34 | appBar: AppBar(title: const Text(App.title)), 35 | body: LlmChatView( 36 | provider: FirebaseProvider( 37 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /example/lib/logging/logging.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Logging'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | ChatPage({super.key}); 31 | final _provider = FirebaseProvider( 32 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 33 | ); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar(title: const Text(App.title)), 39 | body: LlmChatView(provider: _provider, messageSender: _logMessage), 40 | ); 41 | } 42 | 43 | Stream _logMessage( 44 | String prompt, { 45 | required Iterable attachments, 46 | }) async* { 47 | // log the message and attachments 48 | debugPrint('# Sending Message'); 49 | debugPrint('## Prompt\n$prompt'); 50 | debugPrint('## Attachments\n${attachments.map((a) => a.toString())}'); 51 | 52 | // forward the message on to the provider 53 | final response = _provider.sendMessageStream( 54 | prompt, 55 | attachments: attachments, 56 | ); 57 | 58 | // log the response 59 | final text = response.join(); 60 | debugPrint('## Response\n$text'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Google Gemini AI'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | @override 33 | Widget build(BuildContext context) => Scaffold( 34 | appBar: AppBar(title: const Text(App.title)), 35 | body: LlmChatView( 36 | provider: FirebaseProvider( 37 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /example/lib/recipes/data/recipe_data.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:uuid/uuid.dart'; 8 | 9 | class Recipe { 10 | Recipe({ 11 | required this.id, 12 | required this.title, 13 | required this.description, 14 | required this.ingredients, 15 | required this.instructions, 16 | this.tags = const [], 17 | this.notes = '', 18 | }); 19 | 20 | Recipe.empty(String id) 21 | : this( 22 | id: id, 23 | title: '', 24 | description: '', 25 | ingredients: [], 26 | instructions: [], 27 | tags: [], 28 | notes: '', 29 | ); 30 | 31 | factory Recipe.fromJson(Map json) => Recipe( 32 | id: json['id'] ?? const Uuid().v4(), 33 | title: json['title'], 34 | description: json['description'], 35 | ingredients: List.from(json['ingredients']), 36 | instructions: List.from(json['instructions']), 37 | tags: json['tags'] == null ? [] : List.from(json['tags']), 38 | notes: json['notes'] ?? '', 39 | ); 40 | 41 | final String id; 42 | final String title; 43 | final String description; 44 | final List ingredients; 45 | final List instructions; 46 | final List tags; 47 | final String notes; 48 | 49 | Map toJson() => { 50 | 'id': id, 51 | 'title': title, 52 | 'description': description, 53 | 'ingredients': ingredients, 54 | 'instructions': instructions, 55 | 'tags': tags, 56 | 'notes': notes, 57 | }; 58 | 59 | static Future> loadFrom(String json) async { 60 | final jsonList = jsonDecode(json) as List; 61 | return [for (final json in jsonList) Recipe.fromJson(json)]; 62 | } 63 | 64 | @override 65 | String toString() => '''# $title 66 | $description 67 | 68 | ## Ingredients 69 | ${ingredients.join('\n')} 70 | 71 | ## Instructions 72 | ${instructions.join('\n')} 73 | '''; 74 | } 75 | 76 | class RecipeEmbedding { 77 | RecipeEmbedding({required this.id, required this.embedding}); 78 | 79 | factory RecipeEmbedding.fromJson(Map json) => 80 | RecipeEmbedding( 81 | id: json['id'], 82 | embedding: List.from(json['embedding']), 83 | ); 84 | 85 | final String id; 86 | final List embedding; 87 | 88 | static Future> loadFrom(String json) async { 89 | final jsonList = jsonDecode(json) as List; 90 | return [for (final json in jsonList) RecipeEmbedding.fromJson(json)]; 91 | } 92 | 93 | Map toJson() => {'id': id, 'embedding': embedding}; 94 | } 95 | -------------------------------------------------------------------------------- /example/lib/recipes/data/recipe_repository.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:io' as io; 8 | 9 | import 'package:flutter/foundation.dart'; 10 | import 'package:flutter/services.dart' show rootBundle; 11 | import 'package:path/path.dart' as path; 12 | import 'package:path_provider/path_provider.dart' as pp; 13 | 14 | import 'recipe_data.dart'; 15 | 16 | class RecipeRepository { 17 | static const newRecipeID = '__NEW_RECIPE__'; 18 | static const _fileName = 'recipes.json'; 19 | 20 | static const _assetFileName = 'assets/recipes_default.json'; 21 | 22 | static List? _recipes; 23 | static final items = ValueNotifier>([]); 24 | 25 | static Future init() async { 26 | assert(_recipes == null, 'call init() only once'); 27 | _recipes = await _loadRecipes(); 28 | items.value = _recipes!; 29 | } 30 | 31 | static Iterable get recipes { 32 | assert(_recipes != null, 'call init() first'); 33 | return _recipes!; 34 | } 35 | 36 | static Recipe getRecipe(String recipeId) { 37 | assert(_recipes != null, 'call init() first'); 38 | if (recipeId == newRecipeID) return Recipe.empty(newRecipeID); 39 | return _recipes!.singleWhere((r) => r.id == recipeId); 40 | } 41 | 42 | static Future addNewRecipe(Recipe newRecipe) async { 43 | assert(_recipes != null, 'call init() first'); 44 | _recipes!.add(newRecipe); 45 | await _saveRecipes(); 46 | } 47 | 48 | static Future updateRecipe(Recipe recipe) async { 49 | assert(_recipes != null, 'call init() first'); 50 | final i = _recipes!.indexWhere((r) => r.id == recipe.id); 51 | assert(i >= 0); 52 | _recipes![i] = recipe; 53 | await _saveRecipes(); 54 | } 55 | 56 | static Future deleteRecipe(Recipe recipe) async { 57 | assert(_recipes != null, 'call init() first'); 58 | final removed = _recipes!.remove(recipe); 59 | assert(removed); 60 | await _saveRecipes(); 61 | } 62 | 63 | static Future get _recipeFile async { 64 | final directory = await pp.getApplicationSupportDirectory(); 65 | return io.File(path.join(directory.path, _fileName)); 66 | } 67 | 68 | static Future> _loadRecipes() async { 69 | // seed empty recipe file w/ sample recipes; note: we're not loading from a 70 | // file on the web; all recipes are in memory for the sessions only 71 | late final String contents; 72 | if (!kIsWeb) { 73 | final recipeFile = await _recipeFile; 74 | contents = 75 | await recipeFile.exists() 76 | ? await recipeFile.readAsString() 77 | : await rootBundle.loadString(_assetFileName); 78 | } else { 79 | contents = await rootBundle.loadString(_assetFileName); 80 | } 81 | 82 | final jsonList = json.decode(contents) as List; 83 | return jsonList.map((json) => Recipe.fromJson(json)).toList(); 84 | } 85 | 86 | static Future _saveRecipes() async { 87 | // note: we're not saving to a file on the web; all recipes are in memory 88 | // for the sessions only 89 | if (!kIsWeb) { 90 | final file = await _recipeFile; 91 | final jsonString = json.encode(recipes.map((r) => r.toJson()).toList()); 92 | await file.writeAsString(jsonString); 93 | } 94 | 95 | // notify listeners that the recipes have changed 96 | items.value = []; 97 | items.value = _recipes!; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/lib/recipes/data/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class Settings { 4 | Settings._(); 5 | 6 | static SharedPreferencesWithCache? _prefs; 7 | 8 | static Future init() async { 9 | assert(_prefs == null, 'call Settings.init() exactly once'); 10 | _prefs = await SharedPreferencesWithCache.create( 11 | cacheOptions: SharedPreferencesWithCacheOptions(), 12 | ); 13 | } 14 | 15 | static String get foodPreferences { 16 | assert(_prefs != null, 'call Settings.init() exactly once'); 17 | return _prefs!.getString('foodPreferences') ?? ''; 18 | } 19 | 20 | static Future setFoodPreferences(String value) async { 21 | assert(_prefs != null, 'call Settings.init() exactly once'); 22 | await _prefs!.setString('foodPreferences', value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/lib/recipes/pages/split_or_tabs.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:split_view/split_view.dart'; 3 | 4 | class SplitOrTabs extends StatefulWidget { 5 | const SplitOrTabs({required this.tabs, required this.children, super.key}); 6 | final List tabs; 7 | final List children; 8 | 9 | @override 10 | State createState() => _SplitOrTabsState(); 11 | } 12 | 13 | class _SplitOrTabsState extends State 14 | with SingleTickerProviderStateMixin { 15 | late TabController _tabController; 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | _tabController = TabController(length: widget.tabs.length, vsync: this); 21 | } 22 | 23 | @override 24 | void dispose() { 25 | _tabController.dispose(); 26 | super.dispose(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) => 31 | MediaQuery.of(context).size.width > 600 32 | ? SplitView( 33 | viewMode: SplitViewMode.Horizontal, 34 | gripColor: Colors.transparent, 35 | indicator: SplitIndicator( 36 | viewMode: SplitViewMode.Horizontal, 37 | color: Colors.grey, 38 | ), 39 | gripColorActive: Colors.transparent, 40 | activeIndicator: SplitIndicator( 41 | viewMode: SplitViewMode.Horizontal, 42 | isActive: true, 43 | color: Colors.black, 44 | ), 45 | children: widget.children, 46 | ) 47 | : Column( 48 | children: [ 49 | TabBar(controller: _tabController, tabs: widget.tabs), 50 | Expanded( 51 | child: TabBarView( 52 | controller: _tabController, 53 | children: widget.children, 54 | ), 55 | ), 56 | ], 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /example/lib/recipes/recipes.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_core/firebase_core.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:go_router/go_router.dart'; 8 | 9 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 10 | import '../firebase_options.dart'; 11 | import 'data/recipe_repository.dart'; 12 | import 'data/settings.dart'; 13 | import 'pages/edit_recipe_page.dart'; 14 | import 'pages/home_page.dart'; 15 | 16 | void main() async { 17 | WidgetsFlutterBinding.ensureInitialized(); 18 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 19 | await Settings.init(); 20 | await RecipeRepository.init(); 21 | runApp(App()); 22 | } 23 | 24 | class App extends StatelessWidget { 25 | App({super.key}); 26 | 27 | final _router = GoRouter( 28 | routes: [ 29 | GoRoute( 30 | name: 'home', 31 | path: '/', 32 | builder: (BuildContext context, _) => const HomePage(), 33 | routes: [ 34 | GoRoute( 35 | name: 'edit', 36 | path: 'edit/:recipe', 37 | builder: (context, state) { 38 | final recipeId = state.pathParameters['recipe']!; 39 | final recipe = RecipeRepository.getRecipe(recipeId); 40 | return EditRecipePage(recipe: recipe); 41 | }, 42 | ), 43 | ], 44 | ), 45 | ], 46 | ); 47 | 48 | @override 49 | Widget build(BuildContext context) => 50 | MaterialApp.router(routerConfig: _router); 51 | } 52 | -------------------------------------------------------------------------------- /example/lib/recipes/views/recipe_content_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | 7 | import '../data/recipe_data.dart'; 8 | 9 | class RecipeContentView extends StatelessWidget { 10 | const RecipeContentView({super.key, required this.recipe}); 11 | 12 | final Recipe recipe; 13 | static const mobileBreakpoint = 600; 14 | 15 | @override 16 | Widget build(BuildContext context) => Padding( 17 | padding: const EdgeInsets.all(16), 18 | child: LayoutBuilder( 19 | builder: 20 | (context, constraints) => 21 | constraints.maxWidth < mobileBreakpoint 22 | ? SingleChildScrollView( 23 | child: Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | spacing: 16, 26 | children: [ 27 | _RecipeIngredientsView(recipe), 28 | _RecipeInstructionsView(recipe), 29 | ], 30 | ), 31 | ) 32 | : Row( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | spacing: 16, 35 | children: [ 36 | Expanded(child: _RecipeIngredientsView(recipe)), 37 | Expanded(child: _RecipeInstructionsView(recipe)), 38 | ], 39 | ), 40 | ), 41 | ); 42 | } 43 | 44 | class _RecipeIngredientsView extends StatelessWidget { 45 | const _RecipeIngredientsView(this.recipe); 46 | final Recipe recipe; 47 | 48 | @override 49 | Widget build(BuildContext context) => Column( 50 | crossAxisAlignment: CrossAxisAlignment.start, 51 | children: [ 52 | Text('Ingredients:🍎', style: Theme.of(context).textTheme.titleMedium), 53 | ...[for (final ingredient in recipe.ingredients) Text('• $ingredient')], 54 | ], 55 | ); 56 | } 57 | 58 | class _RecipeInstructionsView extends StatelessWidget { 59 | const _RecipeInstructionsView(this.recipe); 60 | final Recipe recipe; 61 | 62 | @override 63 | Widget build(BuildContext context) => Column( 64 | crossAxisAlignment: CrossAxisAlignment.start, 65 | children: [ 66 | Text('Instructions:🥧', style: Theme.of(context).textTheme.titleMedium), 67 | ...[ 68 | for (final entry in recipe.instructions.asMap().entries) 69 | Text('${entry.key + 1}. ${entry.value}'), 70 | ], 71 | ], 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /example/lib/recipes/views/recipe_list_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | import '../data/recipe_data.dart'; 9 | import '../data/recipe_repository.dart'; 10 | import 'recipe_view.dart'; 11 | 12 | class RecipeListView extends StatefulWidget { 13 | final String searchText; 14 | 15 | const RecipeListView({super.key, required this.searchText}); 16 | 17 | @override 18 | State createState() => _RecipeListViewState(); 19 | } 20 | 21 | class _RecipeListViewState extends State { 22 | final _expanded = {}; 23 | 24 | Iterable _filteredRecipes(Iterable recipes) => 25 | recipes 26 | .where( 27 | (recipe) => 28 | recipe.title.toLowerCase().contains( 29 | widget.searchText.toLowerCase(), 30 | ) || 31 | recipe.description.toLowerCase().contains( 32 | widget.searchText.toLowerCase(), 33 | ) || 34 | recipe.tags.any( 35 | (tag) => tag.toLowerCase().contains( 36 | widget.searchText.toLowerCase(), 37 | ), 38 | ), 39 | ) 40 | .toList() 41 | ..sort( 42 | (a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()), 43 | ); 44 | 45 | @override 46 | Widget build(BuildContext context) => 47 | ValueListenableBuilder?>( 48 | valueListenable: RecipeRepository.items, 49 | builder: (context, recipes, child) { 50 | if (recipes == null) { 51 | return const Center(child: CircularProgressIndicator()); 52 | } 53 | 54 | final displayedRecipes = _filteredRecipes(recipes).toList(); 55 | return ListView.builder( 56 | itemCount: displayedRecipes.length, 57 | itemBuilder: (context, index) { 58 | final recipe = displayedRecipes[index]; 59 | final recipeId = recipe.id; 60 | return RecipeView( 61 | key: ValueKey(recipeId), 62 | recipe: recipe, 63 | expanded: _expanded[recipeId] == true, 64 | onExpansionChanged: 65 | (expanded) => _onExpand(recipe.id, expanded), 66 | onEdit: () => _onEdit(recipe), 67 | onDelete: () => _onDelete(recipe), 68 | ); 69 | }, 70 | ); 71 | }, 72 | ); 73 | 74 | void _onExpand(String recipeId, bool expanded) => 75 | _expanded[recipeId] = expanded; 76 | 77 | void _onEdit(Recipe recipe) => 78 | context.goNamed('edit', pathParameters: {'recipe': recipe.id}); 79 | 80 | void _onDelete(Recipe recipe) async { 81 | final shouldDelete = await showDialog( 82 | context: context, 83 | builder: 84 | (context) => AlertDialog( 85 | title: const Text('Delete Recipe'), 86 | content: Text( 87 | 'Are you sure you want to delete the recipe "${recipe.title}"?', 88 | ), 89 | actions: [ 90 | TextButton( 91 | onPressed: () => Navigator.pop(context, false), 92 | child: const Text('Cancel'), 93 | ), 94 | TextButton( 95 | onPressed: () => Navigator.pop(context, true), 96 | child: const Text('Delete'), 97 | ), 98 | ], 99 | ), 100 | ); 101 | 102 | if (shouldDelete == true) await RecipeRepository.deleteRecipe(recipe); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /example/lib/recipes/views/recipe_response_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; 5 | 6 | import '../data/recipe_data.dart'; 7 | import '../data/recipe_repository.dart'; 8 | import 'recipe_content_view.dart'; 9 | 10 | class RecipeResponseView extends StatelessWidget { 11 | const RecipeResponseView(this.response, {super.key}); 12 | 13 | final String response; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final children = []; 18 | String? finalText; 19 | 20 | // created with the response from the LLM as the response streams in, so 21 | // many not be a complete response yet 22 | try { 23 | final map = jsonDecode(response); 24 | final recipesWithText = map['recipes'] as List; 25 | finalText = map['text'] as String?; 26 | 27 | for (final recipeWithText in recipesWithText) { 28 | // extract the text before the recipe 29 | final text = recipeWithText['text'] as String?; 30 | if (text != null && text.isNotEmpty) { 31 | children.add(MarkdownBody(data: text)); 32 | } 33 | 34 | // extract the recipe 35 | final json = recipeWithText['recipe'] as Map; 36 | final recipe = Recipe.fromJson(json); 37 | children.add( 38 | Column( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | Text(recipe.title, style: Theme.of(context).textTheme.titleLarge), 42 | Text(recipe.description), 43 | RecipeContentView(recipe: recipe), 44 | ], 45 | ), 46 | ); 47 | 48 | // add a button to add the recipe to the list 49 | children.add( 50 | OutlinedButton( 51 | onPressed: () => RecipeRepository.addNewRecipe(recipe), 52 | child: const Text('Add Recipe'), 53 | ), 54 | ); 55 | } 56 | } catch (e) { 57 | debugPrint('Error parsing response: $e'); 58 | } 59 | 60 | if (children.isEmpty) { 61 | try { 62 | final map = jsonDecode(response); 63 | finalText = map['text'] as String?; 64 | } catch (e) { 65 | debugPrint('Error parsing response: $e'); 66 | finalText = response; 67 | } 68 | } 69 | 70 | // add the remaining text 71 | if (finalText != null && finalText.isNotEmpty) { 72 | children.add(MarkdownBody(data: finalText)); 73 | } 74 | 75 | return Column( 76 | crossAxisAlignment: CrossAxisAlignment.start, 77 | spacing: 16, 78 | children: children, 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /example/lib/recipes/views/recipe_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | 7 | import '../data/recipe_data.dart'; 8 | import 'recipe_content_view.dart'; 9 | 10 | class RecipeView extends StatelessWidget { 11 | const RecipeView({ 12 | required this.recipe, 13 | required this.expanded, 14 | required this.onExpansionChanged, 15 | required this.onEdit, 16 | required this.onDelete, 17 | super.key, 18 | }); 19 | 20 | final Recipe recipe; 21 | final bool expanded; 22 | final ValueChanged? onExpansionChanged; 23 | final Function() onEdit; 24 | final Function() onDelete; 25 | 26 | @override 27 | Widget build(BuildContext context) => Card( 28 | child: Column( 29 | children: [ 30 | ExpansionTile( 31 | title: Text(recipe.title), 32 | subtitle: Text(recipe.description), 33 | initiallyExpanded: expanded, 34 | onExpansionChanged: onExpansionChanged, 35 | children: [ 36 | RecipeContentView(recipe: recipe), 37 | Padding( 38 | padding: const EdgeInsets.all(8.0), 39 | child: OverflowBar( 40 | spacing: 8, 41 | alignment: MainAxisAlignment.end, 42 | children: [ 43 | ElevatedButton( 44 | onPressed: onDelete, 45 | child: const Text('Delete'), 46 | ), 47 | OutlinedButton(onPressed: onEdit, child: const Text('Edit')), 48 | ], 49 | ), 50 | ), 51 | const SizedBox(height: 16), 52 | ], 53 | ), 54 | ], 55 | ), 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /example/lib/recipes/views/search_box.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | 7 | class SearchBox extends StatefulWidget { 8 | final Function(String) onSearchChanged; 9 | 10 | const SearchBox({super.key, required this.onSearchChanged}); 11 | 12 | @override 13 | State createState() => _SearchBoxState(); 14 | } 15 | 16 | class _SearchBoxState extends State 17 | with AutomaticKeepAliveClientMixin { 18 | @override 19 | bool get wantKeepAlive => true; 20 | 21 | final TextEditingController _searchController = TextEditingController(); 22 | 23 | @override 24 | void dispose() { 25 | _searchController.dispose(); 26 | super.dispose(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | super.build(context); 32 | return Padding( 33 | padding: const EdgeInsets.all(8), 34 | child: TextField( 35 | controller: _searchController, 36 | decoration: const InputDecoration( 37 | labelText: 'Search recipes', 38 | border: OutlineInputBorder(), 39 | suffixIcon: Icon(Icons.search), 40 | ), 41 | onChanged: widget.onSearchChanged, 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/lib/recipes/views/settings_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../data/settings.dart'; 4 | 5 | class SettingsDrawer extends StatelessWidget { 6 | SettingsDrawer({super.key, required this.onSave}); 7 | final VoidCallback onSave; 8 | 9 | final controller = TextEditingController(text: Settings.foodPreferences); 10 | 11 | @override 12 | Widget build(BuildContext context) => Drawer( 13 | child: ListView( 14 | children: [ 15 | const DrawerHeader(child: Text('Food Preferences')), 16 | Padding( 17 | padding: const EdgeInsets.all(8.0), 18 | child: TextField( 19 | controller: controller, 20 | maxLines: 5, 21 | decoration: const InputDecoration( 22 | hintText: 'Enter your food preferences...', 23 | border: OutlineInputBorder(borderSide: BorderSide(width: 1)), 24 | enabledBorder: OutlineInputBorder( 25 | borderSide: BorderSide(width: 1), 26 | ), 27 | focusedBorder: OutlineInputBorder( 28 | borderSide: BorderSide(width: 1), 29 | ), 30 | ), 31 | ), 32 | ), 33 | Align( 34 | alignment: Alignment.centerRight, 35 | child: Padding( 36 | padding: const EdgeInsets.all(8.0), 37 | child: OverflowBar( 38 | spacing: 8, 39 | children: [ 40 | ElevatedButton( 41 | child: const Text('Cancel'), 42 | onPressed: () { 43 | Navigator.of(context).pop(); 44 | }, 45 | ), 46 | OutlinedButton( 47 | child: const Text('Save'), 48 | onPressed: () { 49 | Settings.setFoodPreferences(controller.text); 50 | Navigator.of(context).pop(); 51 | onSave(); 52 | }, 53 | ), 54 | ], 55 | ), 56 | ), 57 | ), 58 | ], 59 | ), 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /example/lib/restricted/restricted.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | /// An example demonstrating how to create a restricted chat interface 14 | /// where attachments and voice notes are disabled. 15 | void main() async { 16 | WidgetsFlutterBinding.ensureInitialized(); 17 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 18 | runApp(const App()); 19 | } 20 | 21 | /// A Flutter application that demonstrates a restricted chat interface. 22 | class App extends StatelessWidget { 23 | const App({super.key}); 24 | 25 | @override 26 | Widget build(BuildContext context) => 27 | MaterialApp(home: const ChatPage(), debugShowCheckedModeBanner: false); 28 | } 29 | 30 | /// A screen that displays a restricted chat interface. 31 | class ChatPage extends StatelessWidget { 32 | const ChatPage({super.key}); 33 | 34 | @override 35 | Widget build(BuildContext context) => Scaffold( 36 | appBar: AppBar(title: const Text('Restricted Chat')), 37 | body: LlmChatView( 38 | provider: FirebaseProvider( 39 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 40 | ), 41 | enableAttachments: false, 42 | enableVoiceNotes: false, 43 | ), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /example/lib/suggestions/suggestions.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Suggestions'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => MaterialApp( 26 | title: title, 27 | home: ChatPage(), 28 | debugShowCheckedModeBanner: false, 29 | ); 30 | } 31 | 32 | class ChatPage extends StatefulWidget { 33 | const ChatPage({super.key}); 34 | 35 | @override 36 | State createState() => _ChatPageState(); 37 | } 38 | 39 | class _ChatPageState extends State { 40 | final _provider = FirebaseProvider( 41 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 42 | ); 43 | 44 | @override 45 | Widget build(BuildContext context) => Scaffold( 46 | appBar: AppBar( 47 | title: const Text(App.title), 48 | actions: [ 49 | IconButton(onPressed: _clearHistory, icon: const Icon(Icons.history)), 50 | ], 51 | ), 52 | body: LlmChatView( 53 | provider: _provider, 54 | suggestions: const [ 55 | 'Tell me a joke.', 56 | 'Write me a limerick.', 57 | 'Perform a haiku.', 58 | ], 59 | ), 60 | ); 61 | 62 | void _clearHistory() => _provider.history = []; 63 | } 64 | -------------------------------------------------------------------------------- /example/lib/vertex/vertex.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Firebase Vertex AI'; 21 | 22 | const App({super.key}); 23 | @override 24 | Widget build(BuildContext context) => 25 | const MaterialApp(title: title, home: ChatPage()); 26 | } 27 | 28 | class ChatPage extends StatelessWidget { 29 | const ChatPage({super.key}); 30 | 31 | @override 32 | Widget build(BuildContext context) => Scaffold( 33 | appBar: AppBar(title: const Text(App.title)), 34 | body: LlmChatView( 35 | provider: FirebaseProvider( 36 | model: FirebaseAI.vertexAI().generativeModel(model: 'gemini-2.0-flash'), 37 | ), 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /example/lib/welcome/welcome.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:firebase_ai/firebase_ai.dart'; 6 | import 'package:firebase_core/firebase_core.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 9 | 10 | // from `flutterfire config`: https://firebase.google.com/docs/flutter/setup 11 | import '../firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 16 | runApp(const App()); 17 | } 18 | 19 | class App extends StatelessWidget { 20 | static const title = 'Example: Welcome Message'; 21 | 22 | const App({super.key}); 23 | 24 | @override 25 | Widget build(BuildContext context) => 26 | const MaterialApp(title: title, home: ChatPage()); 27 | } 28 | 29 | class ChatPage extends StatelessWidget { 30 | const ChatPage({super.key}); 31 | 32 | @override 33 | Widget build(BuildContext context) => Scaffold( 34 | appBar: AppBar(title: const Text(App.title)), 35 | body: LlmChatView( 36 | welcomeMessage: 'Hello and welcome to the Flutter AI Toolkit!', 37 | provider: FirebaseProvider( 38 | model: FirebaseAI.googleAI().generativeModel(model: 'gemini-2.0-flash'), 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import file_selector_macos 9 | import firebase_app_check 10 | import firebase_auth 11 | import firebase_core 12 | import path_provider_foundation 13 | import record_macos 14 | import shared_preferences_foundation 15 | import url_launcher_macos 16 | 17 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 18 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 19 | FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) 20 | FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) 21 | FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) 22 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 23 | RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) 24 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 25 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 26 | } 27 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | # Firebase requires at least 10.15 2 | platform :osx, '10.15' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_macos_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = flutter_ai_toolkit_example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterAiToolkitExample 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.device.audio-input 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | NSMicrophoneUsageDescription 32 | $(PRODUCT_NAME) would like to access your microphone. 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | com.apple.security.device.audio-input 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_ai_toolkit_example 2 | description: >- 3 | Sample apps showing off various features of the Flutter AI Toolkit. 4 | publish_to: none 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ^3.7.0 9 | flutter: '>=3.27.0-0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | flutter_ai_toolkit: 15 | path: .. 16 | cupertino_icons: ^1.0.8 17 | shared_preferences: ^2.3.2 18 | url_launcher: ^6.3.0 19 | go_router: ^14.2.8 20 | uuid: ^4.5.1 21 | path: ^1.9.0 22 | path_provider: ^2.1.4 23 | flutter_markdown_plus: ^1.0.3 24 | google_fonts: ^6.2.1 25 | future_builder_ex: ^5.0.0 26 | split_view: ^3.2.1 27 | firebase_core: ^3.13.0 28 | firebase_ai: ^2.0.0 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | flutter_lints: ^5.0.0 34 | 35 | flutter: 36 | uses-material-design: true 37 | assets: 38 | - assets/recipes_default.json 39 | - assets/halloween-bg.png 40 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_ai_toolkit_example 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_ai_toolkit_example", 3 | "short_name": "flutter_ai_toolkit_example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /font_svg/README.md: -------------------------------------------------------------------------------- 1 | `lib/fonts/FatIcons.ttf` is generated at [fluttericon.com](https://www.fluttericon.com/) from the following files: 2 | - `font_svg/spark-icon.svg` 3 | - `font_svg/submit-icon.svg` 4 | 5 | SVG sources: 6 | - `font_svg/submit-icon.svg` was generated by flipping the svg in `reply_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.svg` horizontally 7 | 8 | - `font_svg/reply_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.svg` was generated by exporting the SVG from [the SVG of Google Symbols "reply" character](https://fonts.google.com/icons?icon.query=reply) 9 | 10 | - `font_svg/spark-icon.svg` was purchased at [the Noun Project](https://thenounproject.com/icon/spark-6645136/) 11 | 12 | Material icons: 13 | The following icons will pulled in from Material and packaged into the FatIcons.ttf so that Material itself wasn't required for a Cupertino app: 14 | - Icons.add 15 | - Icons.attach_file 16 | - Icons.stop 17 | - Icons.mic 18 | - Icons.close 19 | - Icons.camera_alt 20 | - Icons.image 21 | - Icons.edit 22 | - Icons.copy 23 | 24 | more info: 25 | https://stackoverflow.com/a/75657218 26 | -------------------------------------------------------------------------------- /font_svg/reply_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /font_svg/spark-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /font_svg/submit-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /idx-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flutter AI Toolkit", 3 | "description": "Flutter AI Toolkit", 4 | "categories": ["Mobile", "AI & ML"], 5 | "icon": "https://www.gstatic.com/images/branding/productlogos/flutter/v6/192px.svg", 6 | "publisher": "Google LLC", 7 | "host": { 8 | "virtualization": "true" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /idx-template.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: { 2 | packages = [ 3 | pkgs.curl 4 | pkgs.unzip 5 | ]; 6 | bootstrap = '' 7 | mkdir "$out" 8 | cp -rf ${./.}/* "$out" 9 | mkdir "$out/.idx" 10 | mkdir "$out/.vscode" 11 | cp -rf ${./.}/.idx "$out" 12 | cp -rf ${./.}/.vscode "$out" 13 | cp -rf ${./.}/README.md "$out" 14 | cp -rf ${./.}/CHANGELOG.md "$out" 15 | cp -rf ${./.}/LICENSE "$out" 16 | cp -rf ${./.}/pubspec.yaml "$out" 17 | cp -rf ${./.}/analysis_options.yaml "$out" 18 | cp -rf ${./.}/.metadata "$out" 19 | cp -rf ${./.}/.gitignore "$out" 20 | rm "$out/idx-template.nix" 21 | rm "$out/idx-template.json" 22 | chmod -R u+w "$out" 23 | ''; 24 | } 25 | -------------------------------------------------------------------------------- /lib/flutter_ai_toolkit.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /// A library for integrating AI-powered chat functionality into Flutter 6 | /// applications. 7 | /// 8 | /// This library provides a set of tools and widgets to easily incorporate AI 9 | /// language models into your Flutter app, enabling interactive chat experiences 10 | /// with various AI providers. 11 | /// 12 | /// Key components: 13 | /// - LLM providers: Interfaces and implementations for different AI services. 14 | /// - Chat UI: Ready-to-use widgets for displaying chat interfaces. 15 | library; 16 | 17 | export 'src/llm_exception.dart'; 18 | export 'src/providers/interface/chat_message.dart'; 19 | export 'src/providers/providers.dart'; 20 | export 'src/styles/styles.dart'; 21 | export 'src/views/llm_chat_view/llm_chat_view.dart'; 22 | -------------------------------------------------------------------------------- /lib/fonts/FatIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flutter/ai/c71883d7c2fa9ad0d87e8794f21ba6fb0c66b2e0/lib/fonts/FatIcons.ttf -------------------------------------------------------------------------------- /lib/src/chat_view_model/chat_view_model_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'chat_view_model.dart'; 8 | import 'chat_view_model_provider.dart'; 9 | 10 | /// A widget that provides access to a [ChatViewModel] and builds its child 11 | /// using a builder function. 12 | /// 13 | /// This widget is typically used in conjunction with [ChatViewModelProvider] 14 | /// to access the [ChatViewModel] from the widget tree. 15 | @immutable 16 | class ChatViewModelClient extends StatelessWidget { 17 | /// Creates a [ChatViewModelClient]. 18 | /// 19 | /// The [builder] argument must not be null. 20 | const ChatViewModelClient({required this.builder, this.child, super.key}); 21 | 22 | /// A function that builds a widget tree based on the current [ChatViewModel]. 23 | /// 24 | /// This function is called with the current [BuildContext], the 25 | /// [ChatViewModel] obtained from the nearest [ChatViewModelProvider] 26 | /// ancestor, and the optional [child]. 27 | final Widget Function( 28 | BuildContext context, 29 | ChatViewModel viewModel, 30 | Widget? child, 31 | ) 32 | builder; 33 | 34 | /// An optional child widget that can be passed to the [builder] function. 35 | /// 36 | /// This is useful when part of the widget subtree does not depend on the 37 | /// [ChatViewModel] and can be shared across multiple builds. 38 | final Widget? child; 39 | 40 | @override 41 | Widget build(BuildContext context) => 42 | builder(context, ChatViewModelProvider.of(context), child); 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/chat_view_model/chat_view_model_provider.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'chat_view_model.dart'; 8 | 9 | /// A provider widget that makes a [ChatViewModel] available to its descendants. 10 | /// 11 | /// This widget uses the [InheritedWidget] mechanism to efficiently propagate 12 | /// the [ChatViewModel] down the widget tree. 13 | @immutable 14 | class ChatViewModelProvider extends InheritedWidget { 15 | /// Creates a [ChatViewModelProvider]. 16 | /// 17 | /// The [child] and [viewModel] arguments must not be null. 18 | const ChatViewModelProvider({ 19 | required super.child, 20 | required this.viewModel, 21 | super.key, 22 | }); 23 | 24 | /// The [ChatViewModel] to be made available to descendants. 25 | final ChatViewModel viewModel; 26 | 27 | /// Retrieves the [ChatViewModel] from the closest [ChatViewModelProvider] 28 | /// ancestor in the widget tree. 29 | /// 30 | /// This method will assert if no [ChatViewModelProvider] is found in the 31 | /// widget's ancestors. 32 | /// 33 | /// [context] must not be null. 34 | static ChatViewModel of(BuildContext context) { 35 | final viewModel = maybeOf(context); 36 | assert(viewModel != null, 'No ChatViewModelProvider found in context'); 37 | return viewModel!; 38 | } 39 | 40 | /// Retrieves the [ChatViewModel] from the closest [ChatViewModelProvider] 41 | /// ancestor in the widget tree, if one exists. 42 | /// 43 | /// Returns null if no [ChatViewModelProvider] is found. 44 | /// 45 | /// [context] must not be null. 46 | static ChatViewModel? maybeOf(BuildContext context) => 47 | context 48 | .dependOnInheritedWidgetOfExactType() 49 | ?.viewModel; 50 | 51 | @override 52 | bool updateShouldNotify(ChatViewModelProvider oldWidget) => 53 | viewModel != oldWidget.viewModel; 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/dialogs/adaptive_dialog.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/cupertino.dart' 6 | show CupertinoAlertDialog, CupertinoDialogAction, showCupertinoDialog; 7 | import 'package:flutter/material.dart' show AlertDialog, TextButton, showDialog; 8 | import 'package:flutter/widgets.dart'; 9 | 10 | import '../utility.dart'; 11 | 12 | /// A utility class for showing adaptive dialogs that match the current platform 13 | /// style. 14 | @immutable 15 | class AdaptiveAlertDialog { 16 | /// Displays an adaptive dialog with the specified [content] widget. 17 | /// 18 | /// This method selects a Cupertino-style dialog for iOS platforms and a 19 | /// Material-style dialog for other platforms, ensuring a consistent user 20 | /// experience across different devices. 21 | /// 22 | /// Parameters: 23 | /// * [context]: The build context in which to display the dialog. 24 | /// * [content]: The widget to be displayed as the dialog's content. 25 | /// * [showOK]: A boolean flag indicating whether to display an "OK" button 26 | /// in the dialog. Defaults to false. If false, the dialog will be 27 | /// barrier dismissible. 28 | /// 29 | /// Returns a [Future] that resolves with the result value when the dialog is 30 | /// dismissed. 31 | // NOTE: showOK is a fix for https://github.com/flutter/ai/issues/40. 32 | // Otherwise, the context used by Navigator.pop() is the wrong one. 33 | static Future show({ 34 | required BuildContext context, 35 | required Widget content, 36 | bool showOK = false, 37 | }) => 38 | isCupertinoApp(context) 39 | ? showCupertinoDialog( 40 | context: context, 41 | barrierDismissible: !showOK, 42 | builder: 43 | (context) => CupertinoAlertDialog( 44 | content: content, 45 | actions: [ 46 | if (showOK) 47 | CupertinoDialogAction( 48 | onPressed: () => Navigator.pop(context), 49 | child: const Text('OK'), 50 | ), 51 | ], 52 | ), 53 | ) 54 | : showDialog( 55 | context: context, 56 | barrierDismissible: !showOK, 57 | builder: 58 | (context) => Builder( 59 | builder: (context) { 60 | return AlertDialog( 61 | content: content, 62 | actions: [ 63 | if (showOK) 64 | TextButton( 65 | onPressed: () => Navigator.pop(context), 66 | child: const Text('OK'), 67 | ), 68 | ], 69 | ); 70 | }, 71 | ), 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/dialogs/adaptive_snack_bar/adaptive_snack_bar.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart' show ScaffoldMessenger, SnackBar; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | import '../../utility.dart'; 9 | import 'cupertino_snack_bar.dart'; 10 | 11 | /// A utility class for showing adaptive snack bars in Flutter applications. 12 | /// 13 | /// This class provides a static method to display snack bars that adapt to the 14 | /// current application environment, showing either a Material Design snack bar 15 | /// or a Cupertino-style snack bar based on the app's context. 16 | @immutable 17 | class AdaptiveSnackBar { 18 | /// Shows an adaptive snack bar with the given message. 19 | /// 20 | /// This method determines whether the app is using Cupertino or Material 21 | /// design and displays an appropriate snack bar. 22 | /// 23 | /// Parameters: 24 | /// * [context]: The build context in which to show the snack bar. 25 | /// * [message]: The text message to display in the snack bar. 26 | static void show(BuildContext context, String message) { 27 | if (isCupertinoApp(context)) { 28 | _showCupertinoSnackBar(context: context, message: message); 29 | } else { 30 | ScaffoldMessenger.of( 31 | context, 32 | ).showSnackBar(SnackBar(content: Text(message))); 33 | } 34 | } 35 | 36 | static void _showCupertinoSnackBar({ 37 | required BuildContext context, 38 | required String message, 39 | int durationMillis = 4000, 40 | }) { 41 | const animationDurationMillis = 200; 42 | final overlayEntry = OverlayEntry( 43 | builder: 44 | (context) => CupertinoSnackBar( 45 | message: message, 46 | animationDurationMillis: animationDurationMillis, 47 | waitDurationMillis: durationMillis, 48 | ), 49 | ); 50 | Future.delayed( 51 | Duration(milliseconds: durationMillis + 2 * animationDurationMillis), 52 | overlayEntry.remove, 53 | ); 54 | Overlay.of(context).insert(overlayEntry); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/dialogs/adaptive_snack_bar/cupertino_snack_bar.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/cupertino.dart'; 6 | 7 | /// A widget that displays a Cupertino-style snack bar. 8 | /// 9 | /// This widget creates an animated snack bar that slides up from the bottom of 10 | /// the screen, displays a message for a specified duration, and then slides 11 | /// back down. 12 | /// 13 | /// The snack bar uses Cupertino styling to match iOS design guidelines. 14 | @immutable 15 | class CupertinoSnackBar extends StatefulWidget { 16 | /// Creates a [CupertinoSnackBar]. 17 | /// 18 | /// All parameters are required: 19 | /// * [message] is the text to display in the snack bar. 20 | /// * [animationDurationMillis] defines how long the slide animations take. 21 | /// * [waitDurationMillis] sets how long the snack bar stays visible before 22 | /// dismissing. 23 | const CupertinoSnackBar({ 24 | required this.message, 25 | required this.animationDurationMillis, 26 | required this.waitDurationMillis, 27 | super.key, 28 | }); 29 | 30 | /// The message to display in the snack bar. 31 | final String message; 32 | 33 | /// The duration of the slide-in and slide-out animations in milliseconds. 34 | final int animationDurationMillis; 35 | 36 | /// The duration for which the snack bar remains visible in milliseconds. 37 | final int waitDurationMillis; 38 | 39 | @override 40 | State createState() => _CupertinoSnackBarState(); 41 | } 42 | 43 | class _CupertinoSnackBarState extends State { 44 | bool show = false; 45 | 46 | @override 47 | void initState() { 48 | super.initState(); 49 | Future.microtask(() => setState(() => show = true)); 50 | Future.delayed(Duration(milliseconds: widget.waitDurationMillis), () { 51 | if (mounted) { 52 | setState(() => show = false); 53 | } 54 | }); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) => AnimatedPositioned( 59 | bottom: show ? 8.0 : -50.0, 60 | left: 8, 61 | right: 8, 62 | curve: show ? Curves.linearToEaseOut : Curves.easeInToLinear, 63 | duration: Duration(milliseconds: widget.animationDurationMillis), 64 | child: CupertinoPopupSurface( 65 | child: Padding( 66 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), 67 | child: Text( 68 | widget.message, 69 | style: const TextStyle( 70 | fontSize: 14, 71 | color: CupertinoColors.secondaryLabel, 72 | ), 73 | textAlign: TextAlign.center, 74 | ), 75 | ), 76 | ), 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/dialogs/image_preview_dialog.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../providers/interface/attachments.dart'; 8 | 9 | /// Displays a dialog to preview the image when the user taps on an attached 10 | /// image. 11 | @immutable 12 | class ImagePreviewDialog extends StatelessWidget { 13 | /// Shows the [ImagePreviewDialog] for the given [attachment]. 14 | const ImagePreviewDialog(this.attachment, {super.key}); 15 | 16 | /// The image file attachment to be previewed in the dialog. 17 | final ImageFileAttachment attachment; 18 | 19 | @override 20 | Widget build(BuildContext context) => Padding( 21 | padding: const EdgeInsets.all(8), 22 | child: Center(child: Image.memory(attachment.bytes, fit: BoxFit.contain)), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/llm_exception.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | /// Exception class for LLM-related errors. 8 | /// 9 | /// This class is used to represent exceptions that occur during 10 | /// LLM (Language Learning Model) operations. 11 | @immutable 12 | abstract class LlmException implements Exception { 13 | /// Creates a new [LlmException] with the given error [message]. 14 | /// 15 | /// The [message] parameter is a string describing the error that occurred. 16 | const LlmException._([this.message = '']); 17 | 18 | /// The message describing the error that occurred. 19 | final String message; 20 | 21 | @override 22 | String toString() => 'LlmException: $message'; 23 | } 24 | 25 | /// Exception thrown when an LLM operation is cancelled. 26 | /// 27 | /// This exception is used to indicate that an LLM operation was 28 | /// intentionally cancelled, typically by user action or a timeout. 29 | @immutable 30 | class LlmCancelException extends LlmException { 31 | /// Creates a new [LlmCancelException]. 32 | const LlmCancelException() : super._(); 33 | 34 | @override 35 | String toString() => 'LlmCancelException'; 36 | } 37 | 38 | /// Exception thrown when an LLM operation fails. 39 | /// 40 | /// This exception is used to represent failures in LLM operations 41 | /// that are not due to cancellation, such as network errors or 42 | /// invalid responses from the LLM provider. 43 | @immutable 44 | class LlmFailureException extends LlmException { 45 | /// Creates a new [LlmFailureException] with the given error [message]. 46 | /// 47 | /// The [message] parameter is a string describing the failure that occurred. 48 | const LlmFailureException([super.message]) : super._(); 49 | 50 | @override 51 | String toString() => 'LlmFailureException: $message'; 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/platform_helper/platform_helper.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | export 'platform_helper_web.dart' 6 | if (dart.library.io) 'platform_helper_io.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/platform_helper/platform_helper_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:flutter/widgets.dart'; 8 | import 'package:image_picker/image_picker.dart'; 9 | 10 | /// Deletes a file from the file system. 11 | /// 12 | /// This method takes an [XFile] object and deletes the corresponding file 13 | /// from the file system. It uses the [File] class from dart:io to perform 14 | /// the deletion. 15 | /// 16 | /// Parameters: 17 | /// - file: An [XFile] object representing the file to be deleted. 18 | /// 19 | /// Returns: 20 | /// A [Future] that completes when the file has been deleted. 21 | /// 22 | /// Throws: 23 | /// - [FileSystemException] if the file cannot be deleted. 24 | Future deleteFile(XFile file) async { 25 | await File(file.path).delete(); 26 | } 27 | 28 | /// Checks if the device can take a photo. 29 | /// 30 | /// This method returns `true` if the device supports taking photos using 31 | /// the camera, and `false` otherwise. It uses the [ImagePicker] class 32 | /// to check for camera support. 33 | /// 34 | /// Returns: 35 | /// A [bool] indicating whether the device can take a photo. 36 | bool canTakePhoto() => ImagePicker().supportsImageSource(ImageSource.camera); 37 | 38 | /// Opens a dialog to take a photo using the device's camera. 39 | /// 40 | /// This method displays a camera interface to the user, allowing them to 41 | /// capture a photo. The captured photo is returned as an [XFile] object. 42 | /// 43 | /// Parameters: 44 | /// - context: The build context in which to show the camera dialog. 45 | /// 46 | /// Returns: 47 | /// A [Future] that completes with an [XFile] object representing the 48 | /// captured photo, or `null` if the photo capture was canceled. 49 | Future takePhoto(BuildContext context) => 50 | ImagePicker().pickImage(source: ImageSource.camera); 51 | -------------------------------------------------------------------------------- /lib/src/platform_helper/platform_helper_web.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:cross_file/cross_file.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | import 'package:flutter_picture_taker/flutter_picture_taker.dart'; 8 | 9 | /// Deletes a file from the file system. 10 | /// 11 | /// This method is a no-op on web platforms, as web browsers do not have 12 | /// direct access to the file system. The method is provided for API 13 | /// compatibility with non-web platforms. 14 | /// 15 | /// Parameters: 16 | /// - file: An [XFile] object representing the file to be deleted. 17 | /// This parameter is ignored on web platforms. 18 | /// 19 | /// Returns: 20 | /// A [Future] that completes immediately, as no actual deletion occurs. 21 | Future deleteFile(XFile file) async {} 22 | 23 | /// Checks if the device can take a photo. 24 | /// 25 | /// This method always returns `true` on web platforms, as the capability 26 | /// to take a photo is assumed to be available via the flutter_picture_taker 27 | /// package. 28 | /// 29 | /// Returns: 30 | /// A [bool] indicating whether the device can take a photo. 31 | bool canTakePhoto() => true; 32 | 33 | /// Opens a dialog to take a photo using the device's camera. 34 | /// 35 | /// This method displays a camera interface to the user, allowing them to 36 | /// capture a photo. The captured photo is returned as an [XFile] object. 37 | /// 38 | /// Parameters: 39 | /// - context: The build context in which to show the camera dialog. 40 | /// 41 | /// Returns: 42 | /// A [Future] that completes with an [XFile] object representing the 43 | /// captured photo, or `null` if the photo capture was canceled. 44 | Future takePhoto(BuildContext context) => 45 | showStillCameraDialog(context); 46 | -------------------------------------------------------------------------------- /lib/src/providers/implementations/echo_provider.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import '../../llm_exception.dart'; 8 | import '../interface/attachments.dart'; 9 | import '../interface/chat_message.dart'; 10 | import '../interface/llm_provider.dart'; 11 | 12 | /// A simple LLM provider that echoes the input prompt and attachment 13 | /// information. 14 | /// 15 | /// This provider is primarily used for testing and debugging purposes. 16 | class EchoProvider extends LlmProvider with ChangeNotifier { 17 | /// Creates an [EchoProvider] instance with an optional chat history. 18 | /// 19 | /// The [history] parameter is an optional iterable of [ChatMessage] objects 20 | /// representing the chat history. If provided, it will be converted to a list 21 | /// and stored internally. If not provided, an empty list will be used. 22 | EchoProvider({Iterable? history}) 23 | : _history = List.from(history ?? []); 24 | 25 | final List _history; 26 | 27 | @override 28 | Stream generateStream( 29 | String prompt, { 30 | Iterable attachments = const [], 31 | }) async* { 32 | if (prompt == 'FAILFAST') throw const LlmFailureException('Failing fast!'); 33 | 34 | await Future.delayed(const Duration(milliseconds: 1000)); 35 | yield '# Echo\n'; 36 | 37 | switch (prompt) { 38 | case 'CANCEL': 39 | throw const LlmCancelException(); 40 | case 'FAIL': 41 | throw const LlmFailureException('User requested failure'); 42 | } 43 | 44 | await Future.delayed(const Duration(milliseconds: 1000)); 45 | yield prompt; 46 | 47 | yield '\n\n# Attachments\n${attachments.map((a) => a.toString())}'; 48 | } 49 | 50 | @override 51 | Stream sendMessageStream( 52 | String prompt, { 53 | Iterable attachments = const [], 54 | }) async* { 55 | final userMessage = ChatMessage.user(prompt, attachments); 56 | final llmMessage = ChatMessage.llm(); 57 | _history.addAll([userMessage, llmMessage]); 58 | final response = generateStream(prompt, attachments: attachments); 59 | 60 | // don't write this code if you're targeting the web until this is fixed: 61 | // https://github.com/dart-lang/sdk/issues/47764 62 | // await for (final chunk in chunks) { 63 | // llmMessage.append(chunk); 64 | // yield chunk; 65 | // } 66 | yield* response.map((chunk) { 67 | llmMessage.append(chunk); 68 | return chunk; 69 | }); 70 | 71 | notifyListeners(); 72 | } 73 | 74 | @override 75 | Iterable get history => _history; 76 | 77 | @override 78 | set history(Iterable history) { 79 | _history.clear(); 80 | _history.addAll(history); 81 | notifyListeners(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/providers/interface/llm_provider.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'attachments.dart'; 8 | import 'chat_message.dart'; 9 | 10 | /// An abstract class representing a Language Model (LLM) provider. 11 | /// 12 | /// This class defines the interface for interacting with different LLM 13 | /// services. Implementations of this class should provide the logic for 14 | /// generating text responses based on input prompts and optional attachments. 15 | abstract class LlmProvider implements Listenable { 16 | /// Generates a stream of text based on the given prompt and attachments. 17 | /// This method does not interact with a chat or build on any chat history. 18 | /// 19 | /// [prompt] is the input text to generate a response for. 20 | /// [attachments] is an optional iterable of [Attachment] objects to include 21 | /// with the prompt. These can be images, files, or links that provide 22 | /// additional context for the LLM. 23 | /// 24 | /// Returns a [Stream] of [String] containing the generated text chunks. This 25 | /// allows for streaming responses as they are generated by the LLM. 26 | Stream generateStream( 27 | String prompt, { 28 | Iterable attachments, 29 | }); 30 | 31 | /// Generates a stream of text based on the given prompt and attachments. 32 | /// Interacts with a chat and builds on the history of the chat associated 33 | /// with the provider. 34 | /// 35 | /// This method should be implemented to interact with the specific LLM 36 | /// service and generate text responses. 37 | /// 38 | /// [prompt] is the input text to generate a response for. [attachments] is an 39 | /// optional iterable of [Attachment] objects to include with the prompt. 40 | /// These can be images, files, or links that provide additional context for 41 | /// the LLM. 42 | /// 43 | /// Returns a [Stream] of [String] containing the generated text chunks. This 44 | /// allows for streaming responses as they are generated by the LLM. 45 | Stream sendMessageStream( 46 | String prompt, { 47 | Iterable attachments, 48 | }); 49 | 50 | /// Returns an iterable of [ChatMessage] objects representing the chat 51 | /// history. 52 | /// 53 | /// This getter provides access to the conversation history maintained by the 54 | /// LLM provider. The history typically includes both user messages and LLM 55 | /// responses in chronological order. 56 | /// 57 | /// Returns an [Iterable] of [ChatMessage] objects. 58 | Iterable get history; 59 | 60 | /// Sets the chat history to the provided messages. 61 | /// 62 | /// This setter allows updating the conversation history maintained by the LLM 63 | /// provider. The provided [history] replaces the existing history with a new 64 | /// set of messages. 65 | /// 66 | /// [history] is an [Iterable] of [ChatMessage] objects representing the new 67 | /// chat history. 68 | set history(Iterable history); 69 | } 70 | 71 | /// A function that generates a stream of text based on a prompt and 72 | /// attachments. 73 | typedef LlmStreamGenerator = 74 | Stream Function( 75 | String prompt, { 76 | required Iterable attachments, 77 | }); 78 | -------------------------------------------------------------------------------- /lib/src/providers/interface/message_origin.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /// Represents the origin of a chat message. 6 | enum MessageOrigin { 7 | /// Indicates that the message originated from the user. 8 | user, 9 | 10 | /// Indicates that the message originated from the LLM. 11 | llm; 12 | 13 | /// Checks if the message origin is from the user. 14 | bool get isUser => this == MessageOrigin.user; 15 | 16 | /// Checks if the message origin is from the LLM. 17 | bool get isLlm => this == MessageOrigin.llm; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/providers/providers.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | export 'implementations/echo_provider.dart'; 6 | export 'implementations/firebase_provider.dart'; 7 | export 'interface/attachments.dart'; 8 | export 'interface/chat_message.dart'; 9 | export 'interface/llm_provider.dart'; 10 | export 'interface/message_origin.dart'; 11 | -------------------------------------------------------------------------------- /lib/src/styles/action_button_type.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /// Enum representing different types of action buttons in the chat view. 6 | enum ActionButtonType { 7 | /// Button to add content or initiate a new action. 8 | add, 9 | 10 | /// Button to attach a file to the chat. 11 | attachFile, 12 | 13 | /// Button to access the camera for taking photos or videos. 14 | camera, 15 | 16 | /// Button to cancel an ongoing action or input. 17 | stop, 18 | 19 | /// Button to close the current view or dialog. 20 | close, 21 | 22 | /// Button to close an open menu. 23 | closeMenu, 24 | 25 | /// Button to cancel an operation. 26 | cancel, 27 | 28 | /// Button to copy selected text or content. 29 | copy, 30 | 31 | /// Button to edit existing content or settings. 32 | edit, 33 | 34 | /// Button to access the device's photo gallery. 35 | gallery, 36 | 37 | /// Button to start or stop audio recording. 38 | record, 39 | 40 | /// Button to submit the current input or action. 41 | submit, 42 | 43 | /// Disabled button. 44 | disabled, 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/styles/chat_input_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'toolkit_colors.dart'; 8 | import 'toolkit_text_styles.dart'; 9 | 10 | /// Style for the input text box. 11 | @immutable 12 | class ChatInputStyle { 13 | /// Creates an InputBoxStyle. 14 | const ChatInputStyle({ 15 | this.textStyle, 16 | this.hintStyle, 17 | this.hintText, 18 | this.backgroundColor, 19 | this.decoration, 20 | }); 21 | 22 | /// Merges the provided styles with the default styles. 23 | factory ChatInputStyle.resolve( 24 | ChatInputStyle? style, { 25 | ChatInputStyle? defaultStyle, 26 | }) { 27 | defaultStyle ??= ChatInputStyle.defaultStyle(); 28 | return ChatInputStyle( 29 | textStyle: style?.textStyle ?? defaultStyle.textStyle, 30 | hintStyle: style?.hintStyle ?? defaultStyle.hintStyle, 31 | hintText: style?.hintText ?? defaultStyle.hintText, 32 | backgroundColor: style?.backgroundColor ?? defaultStyle.backgroundColor, 33 | decoration: style?.decoration ?? defaultStyle.decoration, 34 | ); 35 | } 36 | 37 | /// Provides a default style. 38 | factory ChatInputStyle.defaultStyle() => ChatInputStyle._lightStyle(); 39 | 40 | /// Provides a default light style. 41 | factory ChatInputStyle._lightStyle() => ChatInputStyle( 42 | textStyle: ToolkitTextStyles.body2, 43 | hintStyle: ToolkitTextStyles.body2.copyWith(color: ToolkitColors.hintText), 44 | hintText: 'Ask me anything...', 45 | backgroundColor: ToolkitColors.containerBackground, 46 | decoration: BoxDecoration( 47 | color: ToolkitColors.containerBackground, 48 | border: Border.all(width: 1, color: ToolkitColors.outline), 49 | borderRadius: BorderRadius.circular(24), 50 | ), 51 | ); 52 | 53 | /// The text style for the input text box. 54 | final TextStyle? textStyle; 55 | 56 | /// The hint text style for the input text box. 57 | final TextStyle? hintStyle; 58 | 59 | /// The hint text for the input text box. 60 | final String? hintText; 61 | 62 | /// The background color of the input box. 63 | final Color? backgroundColor; 64 | 65 | /// The decoration of the input box. 66 | final Decoration? decoration; 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/styles/file_attachment_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'tookit_icons.dart'; 8 | import 'toolkit_colors.dart'; 9 | import 'toolkit_text_styles.dart'; 10 | 11 | /// Style for file attachments in the chat view. 12 | @immutable 13 | class FileAttachmentStyle { 14 | /// Creates a FileAttachmentStyle. 15 | const FileAttachmentStyle({ 16 | this.decoration, 17 | this.icon, 18 | this.iconColor, 19 | this.iconDecoration, 20 | this.filenameStyle, 21 | this.filetypeStyle, 22 | }); 23 | 24 | /// Resolves the FileAttachmentStyle by combining the provided style with 25 | /// default values. 26 | /// 27 | /// This method takes an optional [style] and merges it with the 28 | /// [defaultStyle]. If [defaultStyle] is not provided, it uses 29 | /// [FileAttachmentStyle.defaultStyle]. 30 | /// 31 | /// [style] - The custom FileAttachmentStyle to apply. Can be null. 32 | /// [defaultStyle] - The default FileAttachmentStyle to use as a base. If 33 | /// null, uses [FileAttachmentStyle.defaultStyle]. 34 | /// 35 | /// Returns a new [FileAttachmentStyle] instance with resolved properties. 36 | factory FileAttachmentStyle.resolve( 37 | FileAttachmentStyle? style, { 38 | FileAttachmentStyle? defaultStyle, 39 | }) { 40 | defaultStyle ??= FileAttachmentStyle.defaultStyle(); 41 | return FileAttachmentStyle( 42 | decoration: style?.decoration ?? defaultStyle.decoration, 43 | icon: style?.icon ?? defaultStyle.icon, 44 | iconColor: style?.iconColor ?? defaultStyle.iconColor, 45 | iconDecoration: style?.iconDecoration ?? defaultStyle.iconDecoration, 46 | filenameStyle: style?.filenameStyle ?? defaultStyle.filenameStyle, 47 | filetypeStyle: style?.filetypeStyle ?? defaultStyle.filetypeStyle, 48 | ); 49 | } 50 | 51 | /// Provides a default style. 52 | factory FileAttachmentStyle.defaultStyle() => 53 | FileAttachmentStyle._lightStyle(); 54 | 55 | /// Provides a default light style. 56 | factory FileAttachmentStyle._lightStyle() => FileAttachmentStyle( 57 | decoration: ShapeDecoration( 58 | color: ToolkitColors.fileContainerBackground, 59 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 60 | ), 61 | icon: ToolkitIcons.attach_file, 62 | iconColor: ToolkitColors.darkIcon, 63 | iconDecoration: ShapeDecoration( 64 | color: ToolkitColors.fileAttachmentIconBackground, 65 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), 66 | ), 67 | filenameStyle: ToolkitTextStyles.filename, 68 | filetypeStyle: ToolkitTextStyles.filetype, 69 | ); 70 | 71 | /// The decoration for the file attachment container. 72 | final Decoration? decoration; 73 | 74 | /// The icon to display for the file attachment. 75 | final IconData? icon; 76 | 77 | /// The color of the file attachment icon. 78 | final Color? iconColor; 79 | 80 | /// The decoration for the file attachment icon container. 81 | final Decoration? iconDecoration; 82 | 83 | /// The text style for the filename. 84 | final TextStyle? filenameStyle; 85 | 86 | /// The text style for the filetype. 87 | final TextStyle? filetypeStyle; 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/styles/styles.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | export 'action_button_style.dart'; 6 | export 'action_button_type.dart'; 7 | export 'chat_input_style.dart'; 8 | export 'file_attachment_style.dart'; 9 | export 'llm_chat_view_style.dart'; 10 | export 'llm_message_style.dart'; 11 | export 'suggestion_style.dart'; 12 | export 'user_message_style.dart'; 13 | -------------------------------------------------------------------------------- /lib/src/styles/suggestion_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'toolkit_colors.dart'; 8 | import 'toolkit_text_styles.dart'; 9 | 10 | /// A class that defines the style for suggestions. 11 | @immutable 12 | class SuggestionStyle { 13 | /// Creates a [SuggestionStyle]. 14 | /// 15 | /// The [textStyle] and [decoration] parameters can be used to customize 16 | /// the appearance of the suggestion. 17 | const SuggestionStyle({this.textStyle, this.decoration}); 18 | 19 | /// Resolves the [SuggestionStyle] by merging the provided [style] with the 20 | /// [defaultStyle]. 21 | /// 22 | /// If [style] is null, the [defaultStyle] is used. If [defaultStyle] is not 23 | /// provided, the [defaultStyle] is obtained from 24 | /// [SuggestionStyle.defaultStyle]. 25 | factory SuggestionStyle.resolve( 26 | SuggestionStyle? style, { 27 | SuggestionStyle? defaultStyle, 28 | }) { 29 | defaultStyle ??= SuggestionStyle.defaultStyle(); 30 | return SuggestionStyle( 31 | textStyle: style?.textStyle ?? defaultStyle.textStyle, 32 | decoration: style?.decoration ?? defaultStyle.decoration, 33 | ); 34 | } 35 | 36 | /// Provides a default style. 37 | /// 38 | /// This style is typically used as the base style for suggestions. 39 | factory SuggestionStyle.defaultStyle() => SuggestionStyle._lightStyle(); 40 | 41 | /// Provides a default light style. 42 | /// 43 | /// This style is typically used for suggestions in light mode. 44 | factory SuggestionStyle._lightStyle() => SuggestionStyle( 45 | textStyle: ToolkitTextStyles.body1, 46 | decoration: const BoxDecoration( 47 | color: ToolkitColors.userMessageBackground, 48 | borderRadius: BorderRadius.all(Radius.circular(8)), 49 | ), 50 | ); 51 | 52 | /// The text style for the suggestion. 53 | final TextStyle? textStyle; 54 | 55 | /// The decoration for the suggestion. 56 | final Decoration? decoration; 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/styles/tookit_icons.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // by convention, using the names of the icons as the constant names 6 | // ignore_for_file: constant_identifier_names 7 | 8 | import 'package:flutter/widgets.dart'; 9 | 10 | /// A collection of custom icons used in the Fat application. 11 | /// 12 | /// This class provides a set of static [IconData] constants that can be used 13 | /// to display custom icons in the application. These icons are part of a custom 14 | /// font called 'FatIcons'. 15 | /// Material Design Icons, Copyright (C) Google, Inc 16 | /// Author: Google 17 | /// License: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) 18 | /// Homepage: https://design.google.com/icons/ 19 | /// 20 | @immutable 21 | class ToolkitIcons { 22 | const ToolkitIcons._(); 23 | 24 | static const _kFontFam = 'FatIcons'; 25 | static const String _kFontPkg = 'flutter_ai_toolkit'; 26 | 27 | /// Icon for submitting or sending. 28 | static const IconData submit_icon = IconData( 29 | 0xe800, 30 | fontFamily: _kFontFam, 31 | fontPackage: _kFontPkg, 32 | ); 33 | 34 | /// Icon representing a spark or idea. 35 | static const IconData spark_icon = IconData( 36 | 0xe801, 37 | fontFamily: _kFontFam, 38 | fontPackage: _kFontPkg, 39 | ); 40 | 41 | /// Icon for adding or creating new items. 42 | static const IconData add = IconData( 43 | 0xe802, 44 | fontFamily: _kFontFam, 45 | fontPackage: _kFontPkg, 46 | ); 47 | 48 | /// Icon for attaching files. 49 | static const IconData attach_file = IconData( 50 | 0xe803, 51 | fontFamily: _kFontFam, 52 | fontPackage: _kFontPkg, 53 | ); 54 | 55 | /// Icon for stopping or halting an action. 56 | static const IconData stop = IconData( 57 | 0xe804, 58 | fontFamily: _kFontFam, 59 | fontPackage: _kFontPkg, 60 | ); 61 | 62 | /// Icon representing a microphone. 63 | static const IconData mic = IconData( 64 | 0xe805, 65 | fontFamily: _kFontFam, 66 | fontPackage: _kFontPkg, 67 | ); 68 | 69 | /// Icon for closing or dismissing. 70 | static const IconData close = IconData( 71 | 0xe806, 72 | fontFamily: _kFontFam, 73 | fontPackage: _kFontPkg, 74 | ); 75 | 76 | /// Icon representing a camera. 77 | static const IconData camera_alt = IconData( 78 | 0xe807, 79 | fontFamily: _kFontFam, 80 | fontPackage: _kFontPkg, 81 | ); 82 | 83 | /// Icon representing an image or picture. 84 | static const IconData image = IconData( 85 | 0xe808, 86 | fontFamily: _kFontFam, 87 | fontPackage: _kFontPkg, 88 | ); 89 | 90 | /// Icon for editing. 91 | static const IconData edit = IconData( 92 | 0xe809, 93 | fontFamily: _kFontFam, 94 | fontPackage: _kFontPkg, 95 | ); 96 | 97 | /// Icon for copying content. 98 | static const IconData content_copy = IconData( 99 | 0xe80a, 100 | fontFamily: _kFontFam, 101 | fontPackage: _kFontPkg, 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /lib/src/styles/toolkit_text_styles.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:google_fonts/google_fonts.dart'; 7 | 8 | import 'toolkit_colors.dart'; 9 | 10 | /// A utility class that defines text styles for the Fat design system. 11 | @immutable 12 | abstract final class ToolkitTextStyles { 13 | /// Large display text style. 14 | /// 15 | /// Used for the most prominent text elements, typically headers or titles. 16 | static final TextStyle display = GoogleFonts.roboto( 17 | color: ToolkitColors.enabledText, 18 | fontSize: 32, 19 | fontWeight: FontWeight.w400, 20 | ); 21 | 22 | /// Primary heading text style. 23 | /// 24 | /// Used for main section headings or important subheadings. 25 | static final TextStyle heading1 = GoogleFonts.roboto( 26 | color: ToolkitColors.enabledText, 27 | fontSize: 24, 28 | fontWeight: FontWeight.w400, 29 | ); 30 | 31 | /// Secondary heading text style. 32 | /// 33 | /// Used for subsection headings or less prominent titles. 34 | static final TextStyle heading2 = GoogleFonts.roboto( 35 | color: ToolkitColors.enabledText, 36 | fontSize: 20, 37 | fontWeight: FontWeight.w400, 38 | ); 39 | 40 | /// Primary body text style. 41 | /// 42 | /// Used for the main content text in the application. 43 | static final TextStyle body1 = GoogleFonts.roboto( 44 | color: ToolkitColors.enabledText, 45 | fontSize: 16, 46 | fontWeight: FontWeight.w400, 47 | ); 48 | 49 | /// Code text style. 50 | /// 51 | /// Used for displaying code snippets or monospaced text. 52 | static final TextStyle code = GoogleFonts.robotoMono( 53 | color: ToolkitColors.enabledText, 54 | fontSize: 16, 55 | fontWeight: FontWeight.w400, 56 | ); 57 | 58 | /// Secondary body text style. 59 | /// 60 | /// Used for less prominent body text or supporting information. 61 | static final TextStyle body2 = GoogleFonts.roboto( 62 | color: ToolkitColors.enabledText, 63 | fontSize: 14, 64 | fontWeight: FontWeight.w400, 65 | ); 66 | 67 | /// Tooltip text style. 68 | /// 69 | /// Used for the text of tooltips. 70 | static final TextStyle tooltip = GoogleFonts.roboto( 71 | color: ToolkitColors.tooltipText.withAlpha(230), 72 | fontSize: 14, 73 | fontWeight: FontWeight.w400, 74 | ); 75 | 76 | /// Filename text style. 77 | /// 78 | /// Used for the text of file attachments. 79 | static final TextStyle filename = GoogleFonts.roboto( 80 | color: ToolkitColors.enabledText, 81 | fontSize: 14, 82 | fontWeight: FontWeight.w400, 83 | ); 84 | 85 | /// File type text style. 86 | /// 87 | /// Used for displaying the file type or MIME type of attachments. 88 | static final TextStyle filetype = GoogleFonts.roboto( 89 | color: ToolkitColors.hintText, 90 | fontSize: 14, 91 | fontWeight: FontWeight.w400, 92 | ); 93 | 94 | /// Label text style. 95 | /// 96 | /// Used for small labels, captions, or helper text. 97 | static final TextStyle label = GoogleFonts.roboto( 98 | color: ToolkitColors.enabledText, 99 | fontSize: 12, 100 | fontWeight: FontWeight.w400, 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/styles/user_message_style.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'toolkit_colors.dart'; 8 | import 'toolkit_text_styles.dart'; 9 | 10 | /// Style for user messages. 11 | @immutable 12 | class UserMessageStyle { 13 | /// Creates a UserMessageStyle. 14 | const UserMessageStyle({this.textStyle, this.decoration}); 15 | 16 | /// Resolves the UserMessageStyle by combining the provided style with default 17 | /// values. 18 | /// 19 | /// This method takes an optional [style] and merges it with the 20 | /// [defaultStyle]. If [defaultStyle] is not provided, it uses 21 | /// [UserMessageStyle.defaultStyle]. 22 | /// 23 | /// [style] - The custom UserMessageStyle to apply. Can be null. 24 | /// [defaultStyle] - The default UserMessageStyle to use as a base. If null, 25 | /// uses [UserMessageStyle.defaultStyle]. 26 | /// 27 | /// Returns a new [UserMessageStyle] instance with resolved properties. 28 | factory UserMessageStyle.resolve( 29 | UserMessageStyle? style, { 30 | UserMessageStyle? defaultStyle, 31 | }) { 32 | defaultStyle ??= UserMessageStyle.defaultStyle(); 33 | return UserMessageStyle( 34 | textStyle: style?.textStyle ?? defaultStyle.textStyle, 35 | decoration: style?.decoration ?? defaultStyle.decoration, 36 | ); 37 | } 38 | 39 | /// Provides default style data for user messages. 40 | factory UserMessageStyle.defaultStyle() => UserMessageStyle._lightStyle(); 41 | 42 | /// Provides a default light style. 43 | factory UserMessageStyle._lightStyle() => UserMessageStyle( 44 | textStyle: ToolkitTextStyles.body1, 45 | decoration: const BoxDecoration( 46 | color: ToolkitColors.userMessageBackground, 47 | borderRadius: BorderRadius.only( 48 | topLeft: Radius.circular(20), 49 | topRight: Radius.zero, 50 | bottomLeft: Radius.circular(20), 51 | bottomRight: Radius.circular(20), 52 | ), 53 | ), 54 | ); 55 | 56 | /// The text style for user messages. 57 | final TextStyle? textStyle; 58 | 59 | /// The decoration for user message bubbles. 60 | final Decoration? decoration; 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/utility.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/cupertino.dart' show BuildContext, CupertinoApp; 6 | import 'package:flutter/services.dart'; 7 | import 'package:universal_platform/universal_platform.dart'; 8 | 9 | import 'dialogs/adaptive_snack_bar/adaptive_snack_bar.dart'; 10 | 11 | bool? _isCupertinoApp; 12 | 13 | /// Determines if the current application is a Cupertino-style app. 14 | /// 15 | /// This function checks the widget tree for the presence of a [CupertinoApp] 16 | /// widget. If found, it indicates that the app is using Cupertino (iOS-style) 17 | /// widgets. 18 | /// 19 | /// Parameters: 20 | /// * [context]: The [BuildContext] used to search the widget tree. 21 | /// 22 | /// Returns: A [bool] value. `true` if a [CupertinoApp] is found in the widget 23 | /// tree, `false` otherwise. 24 | bool isCupertinoApp(BuildContext context) { 25 | // caching the result to avoid recomputing it on every call; it's not likely 26 | // to change during the lifetime of the app 27 | _isCupertinoApp ??= 28 | context.findAncestorWidgetOfExactType() != null; 29 | return _isCupertinoApp!; 30 | } 31 | 32 | /// Determines if the current platform is a mobile device (Android or iOS). 33 | /// 34 | /// This constant uses the [UniversalPlatform] package to check the platform. 35 | /// 36 | /// Returns: 37 | /// A [bool] value. `true` if the platform is either Android or iOS, 38 | /// `false` otherwise. 39 | final isMobile = UniversalPlatform.isAndroid || UniversalPlatform.isIOS; 40 | 41 | /// Copies the given text to the clipboard and shows a confirmation message. 42 | /// 43 | /// This function uses the [Clipboard] API to copy the provided [text] to the 44 | /// system clipboard. After copying, it displays a confirmation message using 45 | /// [AdaptiveSnackBar] if the [context] is still mounted. 46 | /// 47 | /// Parameters: 48 | /// * [context]: The [BuildContext] used to show the confirmation message. 49 | /// * [text]: The text to be copied to the clipboard. 50 | /// 51 | /// Returns: A [Future] that completes when the text has been copied to the 52 | /// clipboard and the confirmation message has been shown. 53 | Future copyToClipboard(BuildContext context, String text) async { 54 | await Clipboard.setData(ClipboardData(text: text)); 55 | if (context.mounted) { 56 | AdaptiveSnackBar.show(context, 'Message copied to clipboard'); 57 | } 58 | } 59 | 60 | /// Inverts the given color. 61 | /// 62 | /// This function takes a [Color] object and returns a new [Color] object 63 | /// with the RGB values inverted. The alpha value remains unchanged. 64 | /// 65 | /// Parameters: 66 | /// * [color]: The [Color] to be inverted. This parameter must not be null. 67 | /// 68 | /// Returns: A new [Color] object with the inverted RGB values. 69 | Color? invertColor(Color? color) => 70 | color != null 71 | ? Color.from( 72 | alpha: color.a, 73 | red: 1 - color.r, 74 | green: 1 - color.g, 75 | blue: 1 - color.b, 76 | ) 77 | : null; 78 | -------------------------------------------------------------------------------- /lib/src/views/action_button.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart' show Tooltip; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | import '../styles/action_button_style.dart'; 9 | import '../utility.dart'; 10 | 11 | /// A button widget with an icon. 12 | /// 13 | /// This widget creates a button with a customizable icon, size, decoration, and 14 | /// color. It can be enabled or disabled based on the presence of an [onPressed] 15 | /// callback. 16 | @immutable 17 | class ActionButton extends StatelessWidget { 18 | /// Creates an [ActionButton]. 19 | /// 20 | /// The [onPressed] and [style] parameters must not be null. 21 | /// The [size] parameter defaults to 40 if not provided. 22 | const ActionButton({ 23 | required this.onPressed, 24 | required this.style, 25 | super.key, 26 | this.size = 40, 27 | }); 28 | 29 | /// The callback that is called when the button is tapped. 30 | /// If null, the button will be disabled. 31 | final VoidCallback onPressed; 32 | 33 | /// The style of the button. 34 | final ActionButtonStyle style; 35 | 36 | /// The diameter of the circular button. 37 | final double size; 38 | 39 | @override 40 | Widget build(BuildContext context) => GestureDetector( 41 | onTap: onPressed, 42 | child: Container( 43 | width: size, 44 | height: size, 45 | decoration: style.iconDecoration, 46 | // tooltips aren't a thing in cupertino, so skip it 47 | child: 48 | isCupertinoApp(context) 49 | ? Icon(style.icon, color: style.iconColor, size: size * 0.6) 50 | : Tooltip( 51 | message: style.text, 52 | textStyle: style.textStyle, 53 | child: Icon( 54 | style.icon, 55 | color: style.iconColor, 56 | size: size * 0.6, 57 | ), 58 | ), 59 | ), 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/views/adaptive_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator; 6 | import 'package:flutter/material.dart' show CircularProgressIndicator; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | import '../utility.dart'; 10 | 11 | /// A progress indicator that adapts to the current platform. 12 | /// 13 | @immutable 14 | class AdaptiveCircularProgressIndicator extends StatelessWidget { 15 | /// Creates an adaptive circular progress indicator. 16 | /// 17 | /// This widget will display a [CupertinoActivityIndicator] on iOS 18 | /// and a [CircularProgressIndicator] on other platforms. 19 | /// 20 | /// The [key] parameter is optional and is used to control how one widget 21 | /// replaces another widget in the tree. 22 | const AdaptiveCircularProgressIndicator({required this.color, super.key}); 23 | 24 | /// The color of the progress indicator. 25 | final Color color; 26 | 27 | @override 28 | Widget build(BuildContext context) => 29 | isCupertinoApp(context) 30 | ? CupertinoActivityIndicator(color: color) 31 | : CircularProgressIndicator(color: color); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/views/attachment_view/attachment_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../../providers/interface/attachments.dart'; 8 | import 'file_attatchment_view.dart'; 9 | import 'image_attachment_view.dart'; 10 | 11 | /// A widget that displays an attachment based on its type. 12 | /// 13 | /// This widget determines the appropriate view for the given [attachment] 14 | /// and renders it accordingly. It supports file attachments and image 15 | /// attachments, but throws an exception for link attachments. 16 | @immutable 17 | class AttachmentView extends StatelessWidget { 18 | /// Creates an AttachmentView. 19 | /// 20 | /// The [attachment] parameter must not be null. 21 | const AttachmentView(this.attachment, {super.key}); 22 | 23 | /// The attachment to be displayed. 24 | final Attachment attachment; 25 | 26 | /// The style for the attachment view. 27 | 28 | @override 29 | Widget build(BuildContext context) => switch (attachment) { 30 | (final ImageFileAttachment a) => ImageAttachmentView(a), 31 | (final FileAttachment a) => FileAttachmentView(a), 32 | (LinkAttachment _) => throw Exception('Link attachments not supported'), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/views/attachment_view/file_attatchment_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../../chat_view_model/chat_view_model_client.dart'; 8 | import '../../providers/interface/attachments.dart'; 9 | import '../../styles/file_attachment_style.dart'; 10 | 11 | /// A widget that displays a file attachment. 12 | /// 13 | /// This widget creates a container with a file icon and information about the 14 | /// attached file, such as its name and MIME type. 15 | @immutable 16 | class FileAttachmentView extends StatelessWidget { 17 | /// Creates a FileAttachmentView. 18 | /// 19 | /// The [attachment] parameter must not be null and represents the 20 | /// file attachment to be displayed. 21 | const FileAttachmentView(this.attachment, {super.key}); 22 | 23 | /// The file attachment to be displayed. 24 | final FileAttachment attachment; 25 | 26 | @override 27 | Widget build(BuildContext context) => ChatViewModelClient( 28 | builder: (context, viewModel, child) { 29 | final attachmentStyle = FileAttachmentStyle.resolve( 30 | viewModel.style?.fileAttachmentStyle, 31 | ); 32 | 33 | return Container( 34 | height: 80, 35 | padding: const EdgeInsets.all(8), 36 | decoration: attachmentStyle.decoration, 37 | child: Row( 38 | mainAxisSize: MainAxisSize.min, 39 | spacing: 8, 40 | children: [ 41 | Container( 42 | height: 64, 43 | padding: const EdgeInsets.all(10), 44 | decoration: attachmentStyle.iconDecoration, 45 | child: Icon( 46 | attachmentStyle.icon, 47 | color: attachmentStyle.iconColor, 48 | size: 24, 49 | ), 50 | ), 51 | Flexible( 52 | child: Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | mainAxisAlignment: MainAxisAlignment.center, 55 | children: [ 56 | Text( 57 | attachment.name, 58 | style: attachmentStyle.filenameStyle, 59 | overflow: TextOverflow.ellipsis, 60 | ), 61 | Text( 62 | attachment.mimeType, 63 | style: attachmentStyle.filetypeStyle, 64 | overflow: TextOverflow.ellipsis, 65 | ), 66 | ], 67 | ), 68 | ), 69 | ], 70 | ), 71 | ); 72 | }, 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/views/attachment_view/image_attachment_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/widgets.dart'; 8 | 9 | import '../../dialogs/adaptive_dialog.dart'; 10 | import '../../dialogs/image_preview_dialog.dart'; 11 | import '../../providers/interface/attachments.dart'; 12 | 13 | /// A widget that displays an image attachment. 14 | /// 15 | /// This widget aligns the image to the center-right of its parent and 16 | /// allows the user to tap on the image to open a preview dialog. 17 | @immutable 18 | class ImageAttachmentView extends StatelessWidget { 19 | /// Creates an ImageAttachmentView. 20 | /// 21 | /// The [attachment] parameter must not be null and represents the 22 | /// image file attachment to be displayed. 23 | const ImageAttachmentView(this.attachment, {super.key}); 24 | 25 | /// The image file attachment to be displayed. 26 | final ImageFileAttachment attachment; 27 | 28 | @override 29 | Widget build(BuildContext context) => Align( 30 | alignment: Alignment.centerRight, 31 | child: GestureDetector( 32 | onTap: () => unawaited(_showPreviewDialog(context)), 33 | child: Image.memory(attachment.bytes), 34 | ), 35 | ); 36 | 37 | Future _showPreviewDialog(BuildContext context) async => 38 | AdaptiveAlertDialog.show( 39 | context: context, 40 | content: ImagePreviewDialog(attachment), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/attachments_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../../providers/interface/attachments.dart'; 8 | import 'removable_attachment.dart'; 9 | 10 | /// A widget that displays a horizontal list of attachments with the ability to 11 | /// remove them. 12 | @immutable 13 | class AttachmentsView extends StatelessWidget { 14 | /// Creates an [AttachmentsView]. 15 | /// 16 | /// The [attachments] parameter is required and represents the list of 17 | /// attachments to display. The [onRemove] parameter is a callback function 18 | /// that is called when an attachment is removed. 19 | const AttachmentsView({ 20 | required this.attachments, 21 | required this.onRemove, 22 | super.key, 23 | }); 24 | 25 | /// The list of attachments to display. 26 | final Iterable attachments; 27 | 28 | /// Callback function that is called when an attachment is removed. 29 | /// 30 | /// The removed [Attachment] is passed as an argument to this function. 31 | final Function(Attachment) onRemove; 32 | 33 | @override 34 | Widget build(BuildContext context) => Container( 35 | height: attachments.isNotEmpty ? 104 : 0, 36 | padding: const EdgeInsets.only(top: 12, bottom: 12, left: 12), 37 | child: 38 | attachments.isNotEmpty 39 | ? ListView( 40 | scrollDirection: Axis.horizontal, 41 | children: [ 42 | for (final a in attachments) 43 | RemovableAttachment(attachment: a, onRemove: onRemove), 44 | ], 45 | ) 46 | : const SizedBox(), 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/chat_suggestion_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import '../../chat_view_model/chat_view_model_client.dart'; 4 | import '../../styles/suggestion_style.dart'; 5 | 6 | /// A widget that displays a list of chat suggestions. 7 | /// 8 | /// This widget takes a list of suggestions and a callback function that is 9 | /// triggered when a suggestion is selected. Each suggestion is displayed 10 | /// as a tappable container with padding and a background color. 11 | @immutable 12 | class ChatSuggestionsView extends StatelessWidget { 13 | /// Creates a [ChatSuggestionsView] widget. 14 | /// 15 | /// The [suggestions] parameter is a list of suggestion strings to display. 16 | /// The [onSelectSuggestion] parameter is a callback function that is called 17 | /// when a suggestion is tapped. 18 | const ChatSuggestionsView({ 19 | required this.suggestions, 20 | required this.onSelectSuggestion, 21 | super.key, 22 | }); 23 | 24 | /// The list of suggestions to display. 25 | final List suggestions; 26 | 27 | /// The callback function to call when a suggestion is selected. 28 | final void Function(String suggestion) onSelectSuggestion; 29 | 30 | @override 31 | Widget build(BuildContext context) => ChatViewModelClient( 32 | builder: (context, viewModel, child) { 33 | final suggestionStyle = SuggestionStyle.resolve( 34 | viewModel.style?.suggestionStyle, 35 | ); 36 | return Wrap( 37 | children: [ 38 | for (final suggestion in suggestions) 39 | GestureDetector( 40 | onTap: () => onSelectSuggestion(suggestion), 41 | child: Container( 42 | margin: const EdgeInsets.all(8), 43 | padding: const EdgeInsets.all(8), 44 | decoration: suggestionStyle.decoration, 45 | child: Text( 46 | suggestion, 47 | softWrap: true, 48 | maxLines: 3, 49 | style: suggestionStyle.textStyle, 50 | ), 51 | ), 52 | ), 53 | ], 54 | ); 55 | }, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/editing_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import '../../styles/action_button_style.dart'; 4 | import '../../styles/toolkit_text_styles.dart'; 5 | import '../../utility.dart'; 6 | import '../action_button.dart'; 7 | 8 | /// A widget that displays an editing indicator with a cancel button. 9 | /// 10 | /// This widget is used to show that the user is currently editing a message. 11 | /// It provides a visual indicator with the text "Editing" and a button to 12 | /// cancel the editing action. 13 | /// 14 | /// The [onCancelEdit] callback is triggered when the cancel button is pressed. 15 | /// The [cancelButtonStyle] is used to style the cancel button. 16 | class EditingIndicator extends StatelessWidget { 17 | /// Creates an [EditingIndicator]. 18 | /// 19 | /// The [onCancelEdit] and [cancelButtonStyle] parameters are required. 20 | const EditingIndicator({ 21 | required this.onCancelEdit, 22 | required this.cancelButtonStyle, 23 | super.key, 24 | }); 25 | 26 | /// The callback to be invoked when the cancel button is pressed. 27 | final VoidCallback onCancelEdit; 28 | 29 | /// The style to be applied to the cancel button. 30 | final ActionButtonStyle cancelButtonStyle; 31 | 32 | @override 33 | Widget build(BuildContext context) => Padding( 34 | padding: const EdgeInsets.only(right: 16), 35 | child: Row( 36 | mainAxisAlignment: MainAxisAlignment.end, 37 | crossAxisAlignment: CrossAxisAlignment.end, 38 | spacing: 6, 39 | children: [ 40 | Text( 41 | 'Editing', 42 | style: ToolkitTextStyles.label.copyWith( 43 | color: invertColor(cancelButtonStyle.iconColor), 44 | ), 45 | ), 46 | ActionButton( 47 | onPressed: onCancelEdit, 48 | style: cancelButtonStyle, 49 | size: 16, 50 | ), 51 | ], 52 | ), 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/input_button.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../../styles/llm_chat_view_style.dart'; 8 | import '../action_button.dart'; 9 | import '../adaptive_progress_indicator.dart'; 10 | import 'input_state.dart'; 11 | 12 | /// A button widget that adapts its appearance and behavior based on the current 13 | /// input state. 14 | @immutable 15 | class InputButton extends StatelessWidget { 16 | /// Creates an [InputButton]. 17 | /// 18 | /// All parameters are required: 19 | /// - [inputState]: The current state of the input. 20 | /// - [chatStyle]: The style configuration for the chat interface. 21 | /// - [onSubmitPrompt]: Callback function when submitting a prompt. 22 | /// - [onCancelPrompt]: Callback function when cancelling a prompt. 23 | /// - [onStartRecording]: Callback function when starting audio recording. 24 | /// - [onStopRecording]: Callback function when stopping audio recording. 25 | const InputButton({ 26 | required this.inputState, 27 | required this.chatStyle, 28 | required this.onSubmitPrompt, 29 | required this.onCancelPrompt, 30 | required this.onStartRecording, 31 | required this.onStopRecording, 32 | super.key, 33 | }); 34 | 35 | /// The current state of the input. 36 | final InputState inputState; 37 | 38 | /// The style configuration for the chat interface. 39 | final LlmChatViewStyle chatStyle; 40 | 41 | /// Callback function when submitting a prompt. 42 | final void Function() onSubmitPrompt; 43 | 44 | /// Callback function when cancelling a prompt. 45 | final void Function() onCancelPrompt; 46 | 47 | /// Callback function when starting audio recording. 48 | final void Function() onStartRecording; 49 | 50 | /// Callback function when stopping audio recording. 51 | final void Function() onStopRecording; 52 | 53 | @override 54 | Widget build(BuildContext context) => switch (inputState) { 55 | InputState.canSubmitPrompt => ActionButton( 56 | style: chatStyle.submitButtonStyle!, 57 | onPressed: onSubmitPrompt, 58 | ), 59 | InputState.canCancelPrompt => ActionButton( 60 | style: chatStyle.stopButtonStyle!, 61 | onPressed: onCancelPrompt, 62 | ), 63 | InputState.canStt => ActionButton( 64 | style: chatStyle.recordButtonStyle!, 65 | onPressed: onStartRecording, 66 | ), 67 | InputState.isRecording => ActionButton( 68 | style: chatStyle.stopButtonStyle!, 69 | onPressed: onStopRecording, 70 | ), 71 | InputState.canCancelStt => AdaptiveCircularProgressIndicator( 72 | color: chatStyle.progressIndicatorColor!, 73 | ), 74 | InputState.disabled => ActionButton( 75 | style: chatStyle.disabledButtonStyle!, 76 | onPressed: () {}, 77 | ), 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/input_state.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /// Represents the different states of the chat input. 6 | enum InputState { 7 | /// The input has text and the submit button is enabled. 8 | canSubmitPrompt, 9 | 10 | /// A prompt is being submitted and the cancel button is enabled. 11 | canCancelPrompt, 12 | 13 | /// The input is empty and the microphone button for speech-to-text is 14 | /// enabled. 15 | canStt, 16 | 17 | /// Speech is being recorded and the stop button is enabled. 18 | isRecording, 19 | 20 | /// Speech is being translated to text and a progress indicator is shown. 21 | canCancelStt, 22 | 23 | /// Can't submit, cancel, or record 24 | disabled, 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/removable_attachment.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/widgets.dart'; 8 | 9 | import '../../chat_view_model/chat_view_model_client.dart'; 10 | import '../../dialogs/adaptive_dialog.dart'; 11 | import '../../dialogs/image_preview_dialog.dart'; 12 | import '../../providers/interface/attachments.dart'; 13 | import '../../styles/llm_chat_view_style.dart'; 14 | import '../action_button.dart'; 15 | import '../attachment_view/attachment_view.dart'; 16 | 17 | /// A widget that displays an attachment with a remove button. 18 | @immutable 19 | class RemovableAttachment extends StatelessWidget { 20 | /// Creates a [RemovableAttachment]. 21 | /// 22 | /// The [attachment] parameter is required and represents the attachment to 23 | /// display. The [onRemove] parameter is a callback function that is called 24 | /// when the remove button is pressed. 25 | const RemovableAttachment({ 26 | required this.attachment, 27 | required this.onRemove, 28 | super.key, 29 | }); 30 | 31 | /// The attachment to display. 32 | final Attachment attachment; 33 | 34 | /// Callback function that is called when the remove button is pressed. 35 | /// 36 | /// The [Attachment] to be removed is passed as an argument to this function. 37 | final Function(Attachment) onRemove; 38 | 39 | @override 40 | Widget build(BuildContext context) => Stack( 41 | children: [ 42 | GestureDetector( 43 | onTap: 44 | attachment is ImageFileAttachment 45 | ? () => unawaited(_showPreviewDialog(context)) 46 | : null, 47 | child: Container( 48 | padding: const EdgeInsets.only(right: 12), 49 | height: 80, 50 | child: AttachmentView(attachment), 51 | ), 52 | ), 53 | Padding( 54 | padding: const EdgeInsets.all(2), 55 | child: ChatViewModelClient( 56 | builder: (context, viewModel, child) { 57 | final chatStyle = LlmChatViewStyle.resolve(viewModel.style); 58 | return ActionButton( 59 | style: chatStyle.closeButtonStyle!, 60 | size: 20, 61 | onPressed: () => onRemove(attachment), 62 | ); 63 | }, 64 | ), 65 | ), 66 | ], 67 | ); 68 | 69 | Future _showPreviewDialog(BuildContext context) async => 70 | AdaptiveAlertDialog.show( 71 | context: context, 72 | content: ImagePreviewDialog(attachment as ImageFileAttachment), 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/views/chat_message_view/adaptive_copy_text.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/cupertino.dart' show DefaultCupertinoLocalizations; 4 | import 'package:flutter/material.dart' 5 | show 6 | DefaultMaterialLocalizations, 7 | SelectionArea, 8 | DefaultWidgetsLocalizations; 9 | import 'package:flutter/widgets.dart'; 10 | import 'package:flutter_context_menu/flutter_context_menu.dart'; 11 | 12 | import '../../styles/llm_chat_view_style.dart'; 13 | import '../../utility.dart'; 14 | 15 | /// A widget that displays text with adaptive copy functionality. 16 | /// 17 | /// This widget provides a context menu for copying text to the clipboard on 18 | /// mobile devices, and a selection area for mouse-driven selection on desktop 19 | /// and web platforms. 20 | @immutable 21 | class AdaptiveCopyText extends StatelessWidget { 22 | /// Creates an [AdaptiveCopyText] widget. 23 | /// 24 | /// The [clipboardText] parameter is required and contains the text to be 25 | /// copied to the clipboard. The [child] parameter is required and contains 26 | /// the widget to be displayed. The [chatStyle] parameter is required and 27 | /// contains the style information for the chat. The [onEdit] parameter is 28 | /// optional and contains the callback to be invoked when the text is edited. 29 | const AdaptiveCopyText({ 30 | required this.clipboardText, 31 | required this.child, 32 | required this.chatStyle, 33 | this.onEdit, 34 | super.key, 35 | }); 36 | 37 | /// The text to be copied to the clipboard. 38 | final String clipboardText; 39 | 40 | /// The widget to be displayed. 41 | final Widget child; 42 | 43 | /// The callback to be invoked when the text is edited. 44 | final VoidCallback? onEdit; 45 | 46 | /// The style information for the chat. 47 | final LlmChatViewStyle chatStyle; 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | final contextMenu = ContextMenu( 52 | entries: [ 53 | if (onEdit != null) 54 | MenuItem( 55 | label: 'Edit', 56 | icon: chatStyle.editButtonStyle!.icon, 57 | onSelected: onEdit, 58 | ), 59 | MenuItem( 60 | label: 'Copy', 61 | icon: chatStyle.copyButtonStyle!.icon, 62 | onSelected: () => unawaited(copyToClipboard(context, clipboardText)), 63 | ), 64 | ], 65 | ); 66 | 67 | // On mobile, show the context menu for long-press; 68 | // on desktop and web, show the selection area for mouse-driven selection. 69 | return isMobile 70 | ? ContextMenuRegion(contextMenu: contextMenu, child: child) 71 | : isCupertinoApp(context) 72 | // Ensure MaterialLocalizations is available for SelectionArea 73 | ? Localizations( 74 | locale: Localizations.localeOf(context), 75 | delegates: const [ 76 | DefaultMaterialLocalizations.delegate, 77 | DefaultWidgetsLocalizations.delegate, 78 | DefaultCupertinoLocalizations.delegate, 79 | ], 80 | child: SelectionArea(child: child), 81 | ) 82 | : SelectionArea(child: child); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/views/chat_message_view/user_message_view.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../../chat_view_model/chat_view_model_client.dart'; 8 | import '../../providers/interface/chat_message.dart'; 9 | import '../../styles/styles.dart'; 10 | import '../attachment_view/attachment_view.dart'; 11 | import 'adaptive_copy_text.dart'; 12 | import 'hovering_buttons.dart'; 13 | 14 | /// A widget that displays a user's message in a chat interface. 15 | /// 16 | /// This widget is responsible for rendering the user's message, including any 17 | /// attachments, in a right-aligned layout. It uses a [Row] and [Column] to 18 | /// structure the content, with the message text displayed in a styled 19 | /// container. 20 | @immutable 21 | class UserMessageView extends StatelessWidget { 22 | /// Creates a [UserMessageView]. 23 | /// 24 | /// The [message] parameter is required and contains the [ChatMessage] to be 25 | /// displayed. 26 | const UserMessageView(this.message, {super.key, this.onEdit}); 27 | 28 | /// The chat message to be displayed. 29 | final ChatMessage message; 30 | 31 | /// The callback to be invoked when the message is edited. 32 | final VoidCallback? onEdit; 33 | 34 | @override 35 | Widget build(BuildContext context) => Column( 36 | children: [ 37 | ...[ 38 | for (final attachment in message.attachments) 39 | Padding( 40 | padding: const EdgeInsets.only(bottom: 6), 41 | child: Align( 42 | alignment: Alignment.topRight, 43 | child: SizedBox( 44 | height: 80, 45 | width: 200, 46 | child: AttachmentView(attachment), 47 | ), 48 | ), 49 | ), 50 | ], 51 | ChatViewModelClient( 52 | builder: (context, viewModel, child) { 53 | final text = message.text!; 54 | final chatStyle = LlmChatViewStyle.resolve(viewModel.style); 55 | final userStyle = UserMessageStyle.resolve( 56 | chatStyle.userMessageStyle, 57 | ); 58 | 59 | return Align( 60 | alignment: Alignment.topRight, 61 | child: Padding( 62 | padding: const EdgeInsets.only(right: 16), 63 | child: HoveringButtons( 64 | isUserMessage: true, 65 | chatStyle: chatStyle, 66 | clipboardText: text, 67 | onEdit: onEdit, 68 | child: DecoratedBox( 69 | decoration: userStyle.decoration!, 70 | child: Padding( 71 | padding: const EdgeInsets.only( 72 | left: 16, 73 | right: 16, 74 | top: 12, 75 | bottom: 12, 76 | ), 77 | child: AdaptiveCopyText( 78 | chatStyle: chatStyle, 79 | clipboardText: text, 80 | onEdit: onEdit, 81 | child: Text(text, style: userStyle.textStyle), 82 | ), 83 | ), 84 | ), 85 | ), 86 | ), 87 | ); 88 | }, 89 | ), 90 | ], 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/views/chat_text_field.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/cupertino.dart' show CupertinoTextField; 6 | import 'package:flutter/material.dart' 7 | show InputBorder, InputDecoration, TextField, TextInputAction; 8 | import 'package:flutter/widgets.dart'; 9 | 10 | import '../styles/toolkit_colors.dart'; 11 | import '../utility.dart'; 12 | 13 | /// A text field that adapts to the current app style (Material or Cupertino). 14 | /// 15 | /// This widget will render either a [CupertinoTextField] or a [TextField] 16 | /// depending on whether the app is using Cupertino or Material design. 17 | @immutable 18 | class ChatTextField extends StatelessWidget { 19 | /// Creates an adaptive text field. 20 | /// 21 | /// Many of the parameters are required to ensure consistent behavior 22 | /// across both Cupertino and Material designs. 23 | const ChatTextField({ 24 | required this.minLines, 25 | required this.maxLines, 26 | required this.autofocus, 27 | required this.style, 28 | required this.textInputAction, 29 | required this.controller, 30 | required this.focusNode, 31 | required this.onSubmitted, 32 | required this.hintText, 33 | required this.hintStyle, 34 | required this.hintPadding, 35 | super.key, 36 | }); 37 | 38 | /// The minimum number of lines to show. 39 | final int minLines; 40 | 41 | /// The maximum number of lines to show. 42 | final int maxLines; 43 | 44 | /// Whether the text field should be focused initially. 45 | final bool autofocus; 46 | 47 | /// The style to use for the text being edited. 48 | final TextStyle style; 49 | 50 | /// The type of action button to use for the keyboard. 51 | final TextInputAction textInputAction; 52 | 53 | /// Controls the text being edited. 54 | final TextEditingController controller; 55 | 56 | /// Defines the keyboard focus for this widget. 57 | final FocusNode focusNode; 58 | 59 | /// The text to show when the text field is empty. 60 | final String hintText; 61 | 62 | /// The style to use for the hint text. 63 | final TextStyle hintStyle; 64 | 65 | /// The padding to use for the hint text. 66 | final EdgeInsetsGeometry? hintPadding; 67 | 68 | /// Called when the user submits editable content. 69 | final void Function(String text) onSubmitted; 70 | 71 | @override 72 | Widget build(BuildContext context) => 73 | isCupertinoApp(context) 74 | ? CupertinoTextField( 75 | minLines: minLines, 76 | maxLines: maxLines, 77 | controller: controller, 78 | autofocus: autofocus, 79 | focusNode: focusNode, 80 | onSubmitted: onSubmitted, 81 | style: style, 82 | placeholder: hintText, 83 | placeholderStyle: hintStyle, 84 | padding: hintPadding ?? EdgeInsets.zero, 85 | decoration: BoxDecoration( 86 | border: Border.all(width: 0, color: ToolkitColors.transparent), 87 | ), 88 | textInputAction: textInputAction, 89 | ) 90 | : TextField( 91 | minLines: minLines, 92 | maxLines: maxLines, 93 | controller: controller, 94 | autofocus: autofocus, 95 | focusNode: focusNode, 96 | textInputAction: textInputAction, 97 | onSubmitted: onSubmitted, 98 | style: style, 99 | decoration: InputDecoration( 100 | border: InputBorder.none, 101 | hintText: hintText, 102 | hintStyle: hintStyle, 103 | contentPadding: hintPadding, 104 | isDense: false, 105 | ), 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/views/jumping_dots_progress_indicator/jumping_dot.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// A widget that represents a single jumping dot in the progress indicator. 8 | @immutable 9 | class JumpingDot extends AnimatedWidget { 10 | /// Creates a [JumpingDot] widget. 11 | /// 12 | /// The [animation] parameter is required and controls the vertical movement 13 | /// of the dot. The [color] parameter sets the color of the dot. The 14 | /// [fontSize] parameter determines the size of the dot. 15 | const JumpingDot({ 16 | required Animation animation, 17 | required this.color, 18 | required this.fontSize, 19 | super.key, 20 | }) : super(listenable: animation); 21 | 22 | /// The color of the dot. 23 | final Color color; 24 | 25 | /// The font size of the dot. 26 | final double fontSize; 27 | 28 | Animation get _animation => listenable as Animation; 29 | 30 | @override 31 | Widget build(BuildContext context) => SizedBox( 32 | height: _animation.value + fontSize, 33 | child: Text( 34 | '.', 35 | style: TextStyle( 36 | color: color, 37 | fontSize: fontSize, 38 | height: 1, // Center the text vertically within its line height 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/views/llm_chat_view/llm_response.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import '../../llm_exception.dart'; 8 | 9 | /// Represents a response from an LLM (Language Learning Model). 10 | /// 11 | /// This class manages the streaming of LLM responses, error handling, and 12 | /// cleanup. 13 | class LlmResponse { 14 | /// Creates an LlmResponse. 15 | /// 16 | /// [stream] is the stream of text chunks from the LLM. [onDone] is an 17 | /// optional callback for when the response is complete or encounters an 18 | /// error. 19 | LlmResponse({ 20 | required Stream stream, 21 | required this.onUpdate, 22 | required this.onDone, 23 | }) { 24 | _subscription = stream.listen( 25 | onUpdate, 26 | onDone: () => onDone(null), 27 | cancelOnError: true, 28 | onError: (err) => _close(_exception(err)), 29 | ); 30 | } 31 | 32 | /// Callback function to be called when a new chunk is received from the 33 | /// response stream. 34 | final void Function(String text) onUpdate; 35 | 36 | /// Callback function to be called when the response is complete or encounters 37 | /// an error. 38 | final void Function(LlmException? error) onDone; 39 | 40 | /// Cancels the response stream. 41 | void cancel() => _close(const LlmCancelException()); 42 | 43 | StreamSubscription? _subscription; 44 | 45 | LlmException _exception(dynamic err) => switch (err) { 46 | (LlmCancelException _) => const LlmCancelException(), 47 | (final LlmFailureException ex) => ex, 48 | _ => LlmFailureException(err.toString()), 49 | }; 50 | 51 | void _close(LlmException error) { 52 | assert(_subscription != null); 53 | unawaited(_subscription!.cancel()); 54 | _subscription = null; 55 | onDone.call(error); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/views/response_builder.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// A function type that defines how to build a widget for displaying a response 8 | /// in the chat interface. 9 | /// 10 | /// [context] is the build context, which can be used to access theme data and 11 | /// other contextual information. 12 | /// 13 | /// [response] is the text of the response from the LLM. 14 | /// 15 | /// The function should return a [Widget] that represents the formatted response 16 | /// in the chat interface. 17 | typedef ResponseBuilder = 18 | Widget Function(BuildContext context, String response); 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_ai_toolkit 2 | description: >- 3 | A set of AI chat-related widgets for Flutter apps targeting 4 | mobile, desktop, and web. 5 | version: 0.9.1 6 | repository: https://github.com/flutter/ai 7 | 8 | topics: 9 | - ai 10 | - chat 11 | - gemini 12 | 13 | environment: 14 | sdk: ^3.7.0 15 | flutter: '>=3.27.0' 16 | 17 | dependencies: 18 | cross_file: ^0.3.4+2 19 | file_selector: ^1.0.3 20 | firebase_ai: ^2.0.0 21 | flutter: 22 | sdk: flutter 23 | flutter_context_menu: ^0.2.0 24 | flutter_markdown_plus: ^1.0.3 25 | flutter_picture_taker: ^0.2.0 26 | google_fonts: ^6.2.1 27 | image_picker: ^1.1.2 28 | mime: ^2.0.0 29 | universal_platform: ^1.1.0 30 | uuid: ^4.4.2 31 | waveform_recorder: ^1.3.0 32 | 33 | dev_dependencies: 34 | flutter_lints: ^6.0.0 35 | flutter_test: 36 | sdk: flutter 37 | 38 | flutter: 39 | fonts: 40 | - family: FatIcons 41 | fonts: 42 | - asset: lib/fonts/FatIcons.ttf 43 | -------------------------------------------------------------------------------- /test/smoke_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() { 11 | testWidgets('Smoke Test - Echo Provider', (tester) async { 12 | await tester.pumpWidget( 13 | MaterialApp( 14 | home: Scaffold(body: LlmChatView(provider: SimpleEchoProvider())), 15 | ), 16 | ); 17 | 18 | // Find the text field and enter a message 19 | final textField = find.byWidgetPredicate((widget) => widget is TextField); 20 | expect(textField, findsOneWidget); 21 | await tester.enterText(textField, 'Hello, World!'); 22 | await tester.pump(); 23 | 24 | // Find the submit button by its tooltip and tap it 25 | final submitButton = find.byTooltip('Submit Message'); 26 | expect(submitButton, findsOneWidget); 27 | await tester.tap(submitButton); 28 | await tester.pump(); 29 | 30 | // Check for the response 31 | expect( 32 | find.byWidgetPredicate( 33 | (widget) => 34 | widget is MarkdownBody && 35 | widget.data == 'prompt: Hello, World!\nattachments: []', 36 | ), 37 | findsOneWidget, 38 | ); 39 | }); 40 | } 41 | 42 | class SimpleEchoProvider extends LlmProvider with ChangeNotifier { 43 | SimpleEchoProvider({Iterable? history}) 44 | : _history = List.from(history ?? []); 45 | 46 | final List _history; 47 | 48 | @override 49 | Stream generateStream( 50 | String prompt, { 51 | Iterable attachments = const [], 52 | }) async* { 53 | yield 'prompt: $prompt\n'; 54 | yield 'attachments: ${attachments.isEmpty ? '[]' : attachments.map((a) => a.toString())}'; 55 | } 56 | 57 | @override 58 | Stream sendMessageStream( 59 | String prompt, { 60 | Iterable attachments = const [], 61 | }) async* { 62 | final userMessage = ChatMessage.user(prompt, attachments); 63 | final llmMessage = ChatMessage.llm(); 64 | _history.addAll([userMessage, llmMessage]); 65 | final chunks = generateStream(prompt, attachments: attachments); 66 | await for (final chunk in chunks) { 67 | llmMessage.append(chunk); 68 | yield chunk; 69 | } 70 | notifyListeners(); 71 | } 72 | 73 | @override 74 | Iterable get history => _history; 75 | 76 | @override 77 | set history(Iterable history) { 78 | _history.clear(); 79 | _history.addAll(history); 80 | notifyListeners(); 81 | } 82 | } 83 | --------------------------------------------------------------------------------