├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README └── screenshot.png ├── analysis_options.yaml ├── check-docs.sh ├── deploy-demo.sh ├── example ├── .firebase │ └── hosting.YnVpbGQvd2Vi.cache ├── .gitignore ├── .metadata ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── 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 │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── 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 │ ├── cupertino │ │ └── cupertino.dart │ ├── custom_styles │ │ └── custom_styles.dart │ ├── dark_mode │ │ └── dark_mode.dart │ ├── dark_style.dart │ ├── demo │ │ ├── api_key_page.dart │ │ └── demo.dart │ ├── echo │ │ └── echo.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 │ ├── 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.lock ├── 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 ├── 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_dialog_action.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 │ │ ├── gemini_provider.dart │ │ └── vertex_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 │ ├── action_button.dart │ └── action_button_bar.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 │ ├── 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 /.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/lib/firebase_options.dart 32 | example/firebase.json 33 | example/.firebaserc 34 | example/android/app/google-services.json 35 | example/ios/Runner/GoogleService-Info.plist 36 | example/macos/Runner/GoogleService-Info.plist 37 | example/lib/echo.dart 38 | .flutter-plugins 39 | .flutter-plugins-dependencies 40 | .vscode/settings.json -------------------------------------------------------------------------------- /.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": "recipes", 83 | "cwd": "example", 84 | "request": "launch", 85 | "type": "dart", 86 | "program": "lib/recipes/recipes.dart", 87 | }, 88 | ] 89 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024, the Flutter AI Toolkit project authors. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | 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 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/README/screenshot.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:all_lint_rules_community/all.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - "**/*.g.dart" 6 | - "**/*.freezed.dart" 7 | - "test/.test_coverage.dart" 8 | - "bin/cache/**" 9 | - "lib/generated_plugin_registrant.dart" 10 | errors: 11 | # without ignore here, we cause import of all_lint_rules to warn, because 12 | # some rules conflict; instead, we're explicitly enabling even conflicting 13 | # rules and are fixing the conflicts in this file 14 | included_file_warning: ignore 15 | unintended_html_in_doc_comment: ignore 16 | 17 | linter: 18 | rules: 19 | avoid_types_on_closure_parameters: true # no; prefer use of final instead 20 | prefer_double_quotes: false # Dart prefers single quotes (for some reason) 21 | unnecessary_final: false # love final! 22 | always_specify_types: false # no; prefer use of final instead 23 | prefer_final_parameters: false # I like the sentiment, but too much typing! 24 | prefer_asserts_with_message: false # too lazy for this... 25 | require_trailing_commas: false # not good for things all on one line 26 | public_member_api_docs: true # except for public libs 27 | avoid_classes_with_only_static_members: false # need this; no namespaces 28 | always_put_control_body_on_new_line: false # single line is nice when we can 29 | always_use_package_imports: false # prefer relative imports for local files 30 | avoid_annotating_with_dynamic: false # be explicit about dynamic 31 | avoid_redundant_argument_values: false # sometimes it's nice to be explicit 32 | one_member_abstracts: false # interfaces can have a single method 33 | flutter_style_todos: false # I'm too lazy for this... 34 | diagnostic_describe_all_properties: false # too annoying for StatefulWidget 35 | library_private_types_in_public_api: false # too annoying for StatefulWidget 36 | cascade_invocations: false # not a fan... -------------------------------------------------------------------------------- /check-docs.sh: -------------------------------------------------------------------------------- 1 | dart doc --dry-run 2 | -------------------------------------------------------------------------------- /deploy-demo.sh: -------------------------------------------------------------------------------- 1 | cd example 2 | rm -rf build/web 3 | flutter build web --release --target lib/demo/demo.dart 4 | firebase deploy 5 | cd .. 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 | .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 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | 45 | # custom 46 | gemini_api_key.dart 47 | firebase_options.dart -------------------------------------------------------------------------------- /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: "2663184aa79047d0a33a14a3b607954f8fdd8730" 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: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | analyzer: 11 | errors: 12 | library_private_types_in_public_api: ignore 13 | include: package:flutter_lints/flutter.yaml 14 | 15 | linter: 16 | # The lint rules applied to this project can be customized in the 17 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 18 | # included above or to enable additional rules. A list of all available lints 19 | # and their documentation is published at https://dart.dev/lints. 20 | # 21 | # Instead of disabling a lint rule for the entire project in the 22 | # section below, it can also be suppressed for a single line of code 23 | # or a specific dart file by using the `// ignore: name_of_lint` and 24 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 25 | # producing the lint. 26 | rules: 27 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 28 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options 32 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 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.example" 13 | compileSdk = flutter.compileSdkVersion 14 | ndkVersion = "25.1.8937393" 15 | 16 | compileOptions { 17 | sourceCompatibility = JavaVersion.VERSION_1_8 18 | targetCompatibility = JavaVersion.VERSION_1_8 19 | } 20 | 21 | kotlinOptions { 22 | jvmTarget = JavaVersion.VERSION_1_8 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.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 // flutter.minSdkVersion 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.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 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -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.4-all.zip -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return 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.3.2" 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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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 | # Uncomment this line to define a global platform for your project 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 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Example 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSCameraUsageDescription 30 | $(PRODUCT_NAME) would like to access your camera. 31 | NSMicrophoneUsageDescription 32 | $(PRODUCT_NAME) would like to access your microphone. 33 | NSPhotoLibraryUsageDescription 34 | $(PRODUCT_NAME) would like access to your photos. 35 | UIApplicationSupportsIndirectInputEvents 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIMainStoryboardFile 40 | Main 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /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/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:flutter/cupertino.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | 11 | void main() => runApp(const App()); 12 | 13 | class App extends StatelessWidget { 14 | static const title = 'Example: Cupertino'; 15 | 16 | const App({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) => const CupertinoApp( 20 | title: title, 21 | home: ChatPage(), 22 | ); 23 | } 24 | 25 | class ChatPage extends StatelessWidget { 26 | const ChatPage({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) => CupertinoPageScaffold( 30 | navigationBar: CupertinoNavigationBar( 31 | middle: Text(App.title), 32 | ), 33 | child: LlmChatView( 34 | provider: GeminiProvider( 35 | model: GenerativeModel( 36 | model: 'gemini-1.5-flash', 37 | apiKey: geminiApiKey, 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /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:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | import '../dark_style.dart'; 11 | 12 | void main() => runApp(const App()); 13 | 14 | class App extends StatelessWidget { 15 | static const title = 'Example: Dark Mode'; 16 | static final themeMode = ValueNotifier(ThemeMode.dark); 17 | 18 | const App({super.key}); 19 | 20 | @override 21 | Widget build(BuildContext context) => ValueListenableBuilder( 22 | valueListenable: themeMode, 23 | builder: ( 24 | BuildContext context, 25 | ThemeMode mode, 26 | Widget? child, 27 | ) => 28 | MaterialApp( 29 | title: title, 30 | theme: ThemeData.light(), 31 | darkTheme: ThemeData.dark(), 32 | themeMode: mode, 33 | home: ChatPage(), 34 | debugShowCheckedModeBanner: false, 35 | ), 36 | ); 37 | } 38 | 39 | class ChatPage extends StatefulWidget { 40 | const ChatPage({super.key}); 41 | 42 | @override 43 | State createState() => _ChatPageState(); 44 | } 45 | 46 | class _ChatPageState extends State { 47 | final _provider = GeminiProvider( 48 | model: GenerativeModel( 49 | model: 'gemini-1.5-flash', 50 | apiKey: geminiApiKey, 51 | ), 52 | ); 53 | 54 | final _lightStyle = LlmChatViewStyle.defaultStyle(); 55 | final _darkStyle = darkChatViewStyle(); 56 | 57 | @override 58 | Widget build(BuildContext context) => Scaffold( 59 | appBar: AppBar( 60 | title: const Text(App.title), 61 | actions: [ 62 | IconButton( 63 | onPressed: () => App.themeMode.value = 64 | App.themeMode.value == ThemeMode.light 65 | ? ThemeMode.dark 66 | : ThemeMode.light, 67 | tooltip: App.themeMode.value == ThemeMode.light 68 | ? 'Dark Mode' 69 | : 'Light Mode', 70 | icon: const Icon(Icons.brightness_4_outlined), 71 | ), 72 | ], 73 | ), 74 | body: LlmChatView( 75 | provider: _provider, 76 | style: 77 | App.themeMode.value == ThemeMode.dark ? _darkStyle : _lightStyle, 78 | ), 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /example/lib/demo/api_key_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:gap/gap.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | 6 | class GeminiApiKeyPage extends StatefulWidget { 7 | const GeminiApiKeyPage({ 8 | required this.title, 9 | required this.onApiKey, 10 | super.key, 11 | }); 12 | 13 | final String title; 14 | final void Function(String apiKey) onApiKey; 15 | 16 | @override 17 | State createState() => _GeminiApiKeyPageState(); 18 | } 19 | 20 | class _GeminiApiKeyPageState extends State { 21 | static final url = Uri.parse('https://aistudio.google.com/app/apikey'); 22 | final _controller = TextEditingController(); 23 | 24 | @override 25 | void dispose() { 26 | _controller.dispose(); 27 | super.dispose(); 28 | } 29 | 30 | @override 31 | Widget build(BuildContext context) => Scaffold( 32 | appBar: AppBar(title: Text(widget.title)), 33 | body: Center( 34 | child: ValueListenableBuilder( 35 | valueListenable: _controller, 36 | builder: (context, value, child) => Column( 37 | children: [ 38 | const Text('To run this sample, you need a Gemini API key.\n' 39 | 'Get your Gemini API Key from the following URL:'), 40 | GestureDetector( 41 | onTap: () => launchUrl(url, webOnlyWindowName: '_blank'), 42 | child: MouseRegion( 43 | cursor: SystemMouseCursors.click, 44 | child: Text( 45 | url.toString(), 46 | style: const TextStyle( 47 | color: Colors.blue, 48 | decoration: TextDecoration.underline, 49 | ), 50 | ), 51 | ), 52 | ), 53 | GestureDetector( 54 | onTap: _copyUrl, 55 | child: const MouseRegion( 56 | cursor: SystemMouseCursors.click, 57 | child: Text('(or copy the URL above by tapping HERE)'), 58 | ), 59 | ), 60 | const Gap(16), 61 | const Text('Paste your API key here:'), 62 | SizedBox( 63 | width: 300, 64 | child: TextField( 65 | controller: _controller, 66 | decoration: InputDecoration( 67 | labelText: 'Gemini API Key', 68 | errorText: _isValidApiKey() 69 | ? null 70 | : 'API key must be 39 characters', 71 | ), 72 | onSubmitted: _isValidApiKey() 73 | ? (apiKey) => widget.onApiKey(apiKey) 74 | : null, 75 | ), 76 | ), 77 | const Gap(16), 78 | ElevatedButton( 79 | onPressed: _isValidApiKey() 80 | ? () => widget.onApiKey(_controller.text) 81 | : null, 82 | child: const Text('Submit'), 83 | ), 84 | ], 85 | ), 86 | ), 87 | ), 88 | ); 89 | 90 | bool _isValidApiKey() => _controller.text.length == 39; 91 | 92 | void _copyUrl() { 93 | Clipboard.setData(ClipboardData(text: url.toString())); 94 | ScaffoldMessenger.of(context).showSnackBar( 95 | const SnackBar(content: Text('Copied URL to clipboard')), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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/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:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | 11 | void main() => runApp(const App()); 12 | 13 | class App extends StatelessWidget { 14 | static const title = 'Example: Google Gemini AI'; 15 | 16 | const App({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) => const MaterialApp( 20 | title: title, 21 | home: ChatPage(), 22 | ); 23 | } 24 | 25 | class ChatPage extends StatelessWidget { 26 | const ChatPage({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) => Scaffold( 30 | appBar: AppBar(title: const Text(App.title)), 31 | body: LlmChatView( 32 | provider: GeminiProvider( 33 | model: GenerativeModel( 34 | model: 'gemini-1.5-flash', 35 | apiKey: geminiApiKey, 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | 11 | void main() => runApp(const App()); 12 | 13 | class App extends StatelessWidget { 14 | static const title = 'Example: Logging'; 15 | 16 | const App({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) => MaterialApp( 20 | title: title, 21 | home: ChatPage(), 22 | ); 23 | } 24 | 25 | class ChatPage extends StatelessWidget { 26 | ChatPage({super.key}); 27 | final _provider = GeminiProvider( 28 | model: GenerativeModel( 29 | model: 'gemini-1.5-flash', 30 | apiKey: geminiApiKey, 31 | ), 32 | ); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar(title: const Text(App.title)), 38 | body: LlmChatView( 39 | provider: _provider, 40 | messageSender: _logMessage, 41 | ), 42 | ); 43 | } 44 | 45 | Stream _logMessage( 46 | String prompt, { 47 | required Iterable attachments, 48 | }) async* { 49 | // log the message and attachments 50 | debugPrint('# Sending Message'); 51 | debugPrint('## Prompt\n$prompt'); 52 | debugPrint('## Attachments\n${attachments.map((a) => a.toString())}'); 53 | 54 | // forward the message on to the provider 55 | final response = _provider.sendMessageStream( 56 | prompt, 57 | attachments: attachments, 58 | ); 59 | 60 | // log the response 61 | final text = response.join(); 62 | debugPrint('## Response\n$text'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | 11 | void main() => runApp(const App()); 12 | 13 | class App extends StatelessWidget { 14 | static const title = 'Example: Google Gemini AI'; 15 | 16 | const App({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) => const MaterialApp( 20 | title: title, 21 | home: ChatPage(), 22 | ); 23 | } 24 | 25 | class ChatPage extends StatelessWidget { 26 | const ChatPage({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) => Scaffold( 30 | appBar: AppBar(title: const Text(App.title)), 31 | body: LlmChatView( 32 | provider: GeminiProvider( 33 | model: GenerativeModel( 34 | model: 'gemini-1.5-flash', 35 | apiKey: geminiApiKey, 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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({ 78 | required this.id, 79 | required this.embedding, 80 | }); 81 | 82 | factory RecipeEmbedding.fromJson(Map json) => 83 | RecipeEmbedding( 84 | id: json['id'], 85 | embedding: List.from(json['embedding']), 86 | ); 87 | 88 | final String id; 89 | final List embedding; 90 | 91 | static Future> loadFrom(String json) async { 92 | final jsonList = jsonDecode(json) as List; 93 | return [for (final json in jsonList) RecipeEmbedding.fromJson(json)]; 94 | } 95 | 96 | Map toJson() => { 97 | 'id': id, 98 | 'embedding': embedding, 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /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 = await recipeFile.exists() 75 | ? await recipeFile.readAsString() 76 | : await rootBundle.loadString(_assetFileName); 77 | } else { 78 | contents = await rootBundle.loadString(_assetFileName); 79 | } 80 | 81 | final jsonList = json.decode(contents) as List; 82 | return jsonList.map((json) => Recipe.fromJson(json)).toList(); 83 | } 84 | 85 | static Future _saveRecipes() async { 86 | // note: we're not saving to a file on the web; all recipes are in memory 87 | // for the sessions only 88 | if (!kIsWeb) { 89 | final file = await _recipeFile; 90 | final jsonString = json.encode(recipes.map((r) => r.toJson()).toList()); 91 | await file.writeAsString(jsonString); 92 | } 93 | 94 | // notify listeners that the recipes have changed 95 | items.value = []; 96 | items.value = _recipes!; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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({ 6 | required this.tabs, 7 | required this.children, 8 | super.key, 9 | }); 10 | final List tabs; 11 | final List children; 12 | 13 | @override 14 | State createState() => _SplitOrTabsState(); 15 | } 16 | 17 | class _SplitOrTabsState extends State 18 | with SingleTickerProviderStateMixin { 19 | late TabController _tabController; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | _tabController = TabController(length: widget.tabs.length, vsync: this); 25 | } 26 | 27 | @override 28 | void dispose() { 29 | _tabController.dispose(); 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) => MediaQuery.of(context).size.width > 600 35 | ? SplitView( 36 | viewMode: SplitViewMode.Horizontal, 37 | gripColor: Colors.transparent, 38 | indicator: SplitIndicator( 39 | viewMode: SplitViewMode.Horizontal, 40 | color: Colors.grey, 41 | ), 42 | gripColorActive: Colors.transparent, 43 | activeIndicator: SplitIndicator( 44 | viewMode: SplitViewMode.Horizontal, 45 | isActive: true, 46 | color: Colors.black, 47 | ), 48 | children: widget.children, 49 | ) 50 | : Column( 51 | children: [ 52 | TabBar( 53 | controller: _tabController, 54 | tabs: widget.tabs, 55 | ), 56 | Expanded( 57 | child: TabBarView( 58 | controller: _tabController, 59 | children: widget.children, 60 | ), 61 | ), 62 | ], 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /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:flutter/material.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | 8 | import 'data/recipe_repository.dart'; 9 | import 'data/settings.dart'; 10 | import 'pages/edit_recipe_page.dart'; 11 | import 'pages/home_page.dart'; 12 | 13 | void main() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | await Settings.init(); 16 | await RecipeRepository.init(); 17 | runApp(App()); 18 | } 19 | 20 | class App extends StatelessWidget { 21 | App({super.key}); 22 | 23 | final _router = GoRouter( 24 | routes: [ 25 | GoRoute( 26 | name: 'home', 27 | path: '/', 28 | builder: (BuildContext context, _) => const HomePage(), 29 | routes: [ 30 | GoRoute( 31 | name: 'edit', 32 | path: 'edit/:recipe', 33 | builder: (context, state) { 34 | final recipeId = state.pathParameters['recipe']!; 35 | final recipe = RecipeRepository.getRecipe(recipeId); 36 | return EditRecipePage(recipe: recipe); 37 | }, 38 | ), 39 | ], 40 | ), 41 | ], 42 | ); 43 | 44 | @override 45 | Widget build(BuildContext context) => 46 | MaterialApp.router(routerConfig: _router); 47 | } 48 | -------------------------------------------------------------------------------- /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 | import 'package:gap/gap.dart'; 7 | 8 | import '../data/recipe_data.dart'; 9 | 10 | class RecipeContentView extends StatelessWidget { 11 | const RecipeContentView({ 12 | super.key, 13 | required this.recipe, 14 | }); 15 | 16 | final Recipe recipe; 17 | static const mobileBreakpoint = 600; 18 | 19 | @override 20 | Widget build(BuildContext context) => Padding( 21 | padding: const EdgeInsets.all(16), 22 | child: LayoutBuilder( 23 | builder: (context, constraints) => 24 | constraints.maxWidth < mobileBreakpoint 25 | ? SingleChildScrollView( 26 | child: Column( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | _RecipeIngredientsView(recipe), 30 | const Gap(16), 31 | _RecipeInstructionsView(recipe), 32 | ], 33 | ), 34 | ) 35 | : Row( 36 | crossAxisAlignment: CrossAxisAlignment.start, 37 | children: [ 38 | Expanded(child: _RecipeIngredientsView(recipe)), 39 | const Gap(16), 40 | Expanded(child: _RecipeInstructionsView(recipe)), 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | 47 | class _RecipeIngredientsView extends StatelessWidget { 48 | const _RecipeIngredientsView(this.recipe); 49 | final Recipe recipe; 50 | 51 | @override 52 | Widget build(BuildContext context) => Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | children: [ 55 | Text( 56 | 'Ingredients:🍎', 57 | style: Theme.of(context).textTheme.titleMedium, 58 | ), 59 | ...[ 60 | for (final ingredient in recipe.ingredients) Text('• $ingredient') 61 | ], 62 | ], 63 | ); 64 | } 65 | 66 | class _RecipeInstructionsView extends StatelessWidget { 67 | const _RecipeInstructionsView(this.recipe); 68 | final Recipe recipe; 69 | 70 | @override 71 | Widget build(BuildContext context) => Column( 72 | crossAxisAlignment: CrossAxisAlignment.start, 73 | children: [ 74 | Text( 75 | 'Instructions:🥧', 76 | style: Theme.of(context).textTheme.titleMedium, 77 | ), 78 | ...[ 79 | for (final entry in recipe.instructions.asMap().entries) 80 | Text('${entry.key + 1}. ${entry.value}') 81 | ], 82 | ], 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /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 | _RecipeListViewState createState() => _RecipeListViewState(); 19 | } 20 | 21 | class _RecipeListViewState extends State { 22 | final _expanded = {}; 23 | 24 | Iterable _filteredRecipes(Iterable recipes) => recipes 25 | .where((recipe) => 26 | recipe.title 27 | .toLowerCase() 28 | .contains(widget.searchText.toLowerCase()) || 29 | recipe.description 30 | .toLowerCase() 31 | .contains(widget.searchText.toLowerCase()) || 32 | recipe.tags.any((tag) => 33 | tag.toLowerCase().contains(widget.searchText.toLowerCase()))) 34 | .toList() 35 | ..sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase())); 36 | 37 | @override 38 | Widget build(BuildContext context) => 39 | ValueListenableBuilder?>( 40 | valueListenable: RecipeRepository.items, 41 | builder: (context, recipes, child) { 42 | if (recipes == null) { 43 | return const Center(child: CircularProgressIndicator()); 44 | } 45 | 46 | final displayedRecipes = _filteredRecipes(recipes).toList(); 47 | return ListView.builder( 48 | itemCount: displayedRecipes.length, 49 | itemBuilder: (context, index) { 50 | final recipe = displayedRecipes[index]; 51 | final recipeId = recipe.id; 52 | return RecipeView( 53 | key: ValueKey(recipeId), 54 | recipe: recipe, 55 | expanded: _expanded[recipeId] == true, 56 | onExpansionChanged: (expanded) => 57 | _onExpand(recipe.id, expanded), 58 | onEdit: () => _onEdit(recipe), 59 | onDelete: () => _onDelete(recipe), 60 | ); 61 | }, 62 | ); 63 | }, 64 | ); 65 | 66 | void _onExpand(String recipeId, bool expanded) => 67 | _expanded[recipeId] = expanded; 68 | 69 | void _onEdit(Recipe recipe) => context.goNamed( 70 | 'edit', 71 | pathParameters: {'recipe': recipe.id}, 72 | ); 73 | 74 | void _onDelete(Recipe recipe) async { 75 | final shouldDelete = await showDialog( 76 | context: context, 77 | builder: (context) => AlertDialog( 78 | title: const Text('Delete Recipe'), 79 | content: Text( 80 | 'Are you sure you want to delete the recipe "${recipe.title}"?', 81 | ), 82 | actions: [ 83 | TextButton( 84 | onPressed: () => Navigator.pop(context, false), 85 | child: const Text('Cancel'), 86 | ), 87 | TextButton( 88 | onPressed: () => Navigator.pop(context, true), 89 | child: const Text('Delete'), 90 | ), 91 | ], 92 | ), 93 | ); 94 | 95 | if (shouldDelete == true) await RecipeRepository.deleteRecipe(recipe); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /example/lib/recipes/views/recipe_response_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_markdown/flutter_markdown.dart'; 5 | import 'package:gap/gap.dart'; 6 | 7 | import '../data/recipe_data.dart'; 8 | import '../data/recipe_repository.dart'; 9 | import 'recipe_content_view.dart'; 10 | 11 | class RecipeResponseView extends StatelessWidget { 12 | const RecipeResponseView(this.response, {super.key}); 13 | 14 | final String response; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final children = []; 19 | String? finalText; 20 | 21 | // created with the response from the LLM as the response streams in, so 22 | // many not be a complete response yet 23 | try { 24 | final map = jsonDecode(response); 25 | final recipesWithText = map['recipes'] as List; 26 | finalText = map['text'] as String?; 27 | 28 | for (final recipeWithText in recipesWithText) { 29 | // extract the text before the recipe 30 | final text = recipeWithText['text'] as String?; 31 | if (text != null && text.isNotEmpty) { 32 | children.add(MarkdownBody(data: text)); 33 | } 34 | 35 | // extract the recipe 36 | final json = recipeWithText['recipe'] as Map; 37 | final recipe = Recipe.fromJson(json); 38 | children.add(const Gap(16)); 39 | children.add(Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Text(recipe.title, style: Theme.of(context).textTheme.titleLarge), 43 | Text(recipe.description), 44 | RecipeContentView(recipe: recipe), 45 | ], 46 | )); 47 | 48 | // add a button to add the recipe to the list 49 | children.add(const Gap(16)); 50 | children.add(OutlinedButton( 51 | onPressed: () => RecipeRepository.addNewRecipe(recipe), 52 | child: const Text('Add Recipe'), 53 | )); 54 | children.add(const Gap(16)); 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 | children: children, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | import 'package:gap/gap.dart'; 7 | 8 | import '../data/recipe_data.dart'; 9 | import 'recipe_content_view.dart'; 10 | 11 | class RecipeView extends StatelessWidget { 12 | const RecipeView({ 13 | required this.recipe, 14 | required this.expanded, 15 | required this.onExpansionChanged, 16 | required this.onEdit, 17 | required this.onDelete, 18 | super.key, 19 | }); 20 | 21 | final Recipe recipe; 22 | final bool expanded; 23 | final ValueChanged? onExpansionChanged; 24 | final Function() onEdit; 25 | final Function() onDelete; 26 | 27 | @override 28 | Widget build(BuildContext context) => Card( 29 | child: Column( 30 | children: [ 31 | ExpansionTile( 32 | title: Text(recipe.title), 33 | subtitle: Text(recipe.description), 34 | initiallyExpanded: expanded, 35 | onExpansionChanged: onExpansionChanged, 36 | children: [ 37 | RecipeContentView(recipe: recipe), 38 | Padding( 39 | padding: const EdgeInsets.all(8.0), 40 | child: OverflowBar( 41 | spacing: 8, 42 | alignment: MainAxisAlignment.end, 43 | children: [ 44 | ElevatedButton( 45 | onPressed: onDelete, 46 | child: const Text('Delete'), 47 | ), 48 | OutlinedButton( 49 | onPressed: onEdit, 50 | child: const Text('Edit'), 51 | ), 52 | ], 53 | ), 54 | ), 55 | const Gap(16), 56 | ], 57 | ), 58 | ], 59 | ), 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /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 | _SearchBoxState 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( 10 | text: Settings.foodPreferences, 11 | ); 12 | 13 | @override 14 | Widget build(BuildContext context) => Drawer( 15 | child: ListView( 16 | children: [ 17 | const DrawerHeader(child: Text('Food Preferences')), 18 | Padding( 19 | padding: const EdgeInsets.all(8.0), 20 | child: TextField( 21 | controller: controller, 22 | maxLines: 5, 23 | decoration: const InputDecoration( 24 | hintText: 'Enter your food preferences...', 25 | border: OutlineInputBorder( 26 | borderSide: BorderSide(width: 1), 27 | ), 28 | enabledBorder: OutlineInputBorder( 29 | borderSide: BorderSide(width: 1), 30 | ), 31 | focusedBorder: OutlineInputBorder( 32 | borderSide: BorderSide(width: 1), 33 | ), 34 | ), 35 | ), 36 | ), 37 | Align( 38 | alignment: Alignment.centerRight, 39 | child: Padding( 40 | padding: const EdgeInsets.all(8.0), 41 | child: OverflowBar( 42 | spacing: 8, 43 | children: [ 44 | ElevatedButton( 45 | child: const Text('Cancel'), 46 | onPressed: () { 47 | Navigator.of(context).pop(); 48 | }, 49 | ), 50 | OutlinedButton( 51 | child: const Text('Save'), 52 | onPressed: () { 53 | Settings.setFoodPreferences(controller.text); 54 | Navigator.of(context).pop(); 55 | onSave(); 56 | }, 57 | ), 58 | ], 59 | ), 60 | ), 61 | ), 62 | ], 63 | ), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /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:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | 11 | void main() => runApp(const App()); 12 | 13 | class App extends StatelessWidget { 14 | static const title = 'Example: Suggestions'; 15 | 16 | const App({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) => MaterialApp( 20 | title: title, 21 | home: ChatPage(), 22 | debugShowCheckedModeBanner: false, 23 | ); 24 | } 25 | 26 | class ChatPage extends StatefulWidget { 27 | const ChatPage({super.key}); 28 | 29 | @override 30 | State createState() => _ChatPageState(); 31 | } 32 | 33 | class _ChatPageState extends State { 34 | final _provider = GeminiProvider( 35 | model: GenerativeModel( 36 | model: 'gemini-1.5-flash', 37 | apiKey: geminiApiKey, 38 | ), 39 | ); 40 | 41 | @override 42 | Widget build(BuildContext context) => Scaffold( 43 | appBar: AppBar( 44 | title: const Text(App.title), 45 | actions: [ 46 | IconButton( 47 | onPressed: _clearHistory, 48 | icon: const Icon(Icons.history), 49 | ), 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_core/firebase_core.dart'; 6 | import 'package:firebase_vertexai/firebase_vertexai.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) => const MaterialApp( 25 | title: title, 26 | home: ChatPage(), 27 | ); 28 | } 29 | 30 | class ChatPage extends StatelessWidget { 31 | const ChatPage({super.key}); 32 | 33 | @override 34 | Widget build(BuildContext context) => Scaffold( 35 | appBar: AppBar(title: const Text(App.title)), 36 | body: LlmChatView( 37 | provider: VertexProvider( 38 | model: FirebaseVertexAI.instance.generativeModel( 39 | model: 'gemini-1.5-flash', 40 | ), 41 | ), 42 | ), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /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:flutter/material.dart'; 6 | import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; 7 | import 'package:google_generative_ai/google_generative_ai.dart'; 8 | 9 | import '../gemini_api_key.dart'; 10 | 11 | void main() => runApp(const App()); 12 | 13 | class App extends StatelessWidget { 14 | static const title = 'Example: Welcome Message'; 15 | 16 | const App({super.key}); 17 | 18 | @override 19 | Widget build(BuildContext context) => const MaterialApp( 20 | title: title, 21 | home: ChatPage(), 22 | ); 23 | } 24 | 25 | class ChatPage extends StatelessWidget { 26 | const ChatPage({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) => Scaffold( 30 | appBar: AppBar(title: const Text(App.title)), 31 | body: LlmChatView( 32 | welcomeMessage: 'Hello and welcome to the Flutter AI Toolkit!', 33 | provider: GeminiProvider( 34 | model: GenerativeModel( 35 | model: 'gemini-1.5-flash', 36 | apiKey: geminiApiKey, 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /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_darwin 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 | RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) 24 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 25 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 26 | } 27 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.15' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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 = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 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: "Sample apps showing off various features of the Flutter AI Toolkit." 3 | publish_to: 'none' 4 | version: 0.6.5 5 | 6 | environment: 7 | sdk: ^3.4.4 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | flutter_ai_toolkit: 13 | path: .. 14 | cupertino_icons: ^1.0.8 15 | google_generative_ai: ^0.4.3 16 | firebase_core: ^3.4.0 17 | firebase_vertexai: ^1.0.1 18 | shared_preferences: ^2.3.2 19 | url_launcher: ^6.3.0 20 | gap: ^3.0.1 21 | go_router: ^14.2.8 22 | uuid: ^4.5.1 23 | path: ^1.9.0 24 | path_provider: ^2.1.4 25 | flutter_markdown: ^0.7.4+1 26 | google_fonts: ^6.2.1 27 | future_builder_ex: ^4.0.0 28 | split_view: ^3.2.1 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/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/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 | example 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "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 | -------------------------------------------------------------------------------- /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/providers/interface/chat_message.dart'; 18 | export 'src/providers/providers.dart'; 19 | export 'src/styles/styles.dart'; 20 | export 'src/views/llm_chat_view/llm_chat_view.dart'; 21 | -------------------------------------------------------------------------------- /lib/fonts/FatIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csells/flutter_ai_toolkit/e4efee359ce5ba8c44f0a0bcd8936e9367af326a/lib/fonts/FatIcons.ttf -------------------------------------------------------------------------------- /lib/src/chat_view_model/chat_view_model.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 '../providers/interface/llm_provider.dart'; 8 | import '../styles/llm_chat_view_style.dart'; 9 | import '../views/response_builder.dart'; 10 | 11 | @immutable 12 | 13 | /// A view model class for managing chat interactions and configurations. 14 | /// 15 | /// This class encapsulates the core data and functionality needed for the chat 16 | /// interface, including the LLM provider, style configuration, welcome message, 17 | /// response builder, and message sender. 18 | class ChatViewModel { 19 | /// Creates a new [ChatViewModel] instance. 20 | /// 21 | /// [provider] is the required [LlmProvider] for handling LLM interactions. 22 | /// [style] is the optional [LlmChatViewStyle] for customizing the chat view's 23 | /// appearance. [welcomeMessage] is an optional message displayed when the 24 | /// chat interface is first opened. [responseBuilder] is an optional builder 25 | /// for customizing chat responses. [messageSender] is an optional 26 | /// [LlmStreamGenerator] for sending messages. 27 | const ChatViewModel({ 28 | required this.provider, 29 | required this.style, 30 | required this.welcomeMessage, 31 | required this.responseBuilder, 32 | required this.messageSender, 33 | }); 34 | 35 | /// The LLM provider for the chat interface. 36 | /// 37 | /// This provider is responsible for managing interactions with the language 38 | /// model, including sending and receiving messages. 39 | final LlmProvider provider; 40 | 41 | /// The style configuration for the chat view. 42 | /// 43 | /// Defines visual properties like colors, decorations, and layout parameters 44 | /// for the chat interface. If null, default styling will be applied. 45 | final LlmChatViewStyle? style; 46 | 47 | /// The welcome message to display in the chat interface. 48 | /// 49 | /// This message is shown to users when they first open the chat interface, 50 | /// providing a friendly introduction or prompt. 51 | final String? welcomeMessage; 52 | 53 | /// The builder for the chat response. 54 | /// 55 | /// This builder allows for customization of how chat responses are rendered 56 | /// in the interface, enabling tailored presentation of messages. 57 | final ResponseBuilder? responseBuilder; 58 | 59 | /// The message sender for the chat interface. 60 | /// 61 | /// This optional generator is used to send messages to the LLM, allowing for 62 | /// asynchronous communication and response handling. 63 | final LlmStreamGenerator? messageSender; 64 | 65 | // The following is needed to support the 66 | // ChatViewModelProvider.updateShouldNotify implementation 67 | @override 68 | bool operator ==(Object other) => 69 | identical(this, other) || 70 | (other is ChatViewModel && 71 | other.provider == provider && 72 | other.style == style && 73 | other.welcomeMessage == welcomeMessage && 74 | other.responseBuilder == responseBuilder && 75 | other.messageSender == messageSender); 76 | 77 | // the following is best practices when overriding operator == 78 | @override 79 | int get hashCode => Object.hash( 80 | provider, 81 | style, 82 | welcomeMessage, 83 | responseBuilder, 84 | messageSender, 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /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({ 21 | required this.builder, 22 | this.child, 23 | super.key, 24 | }); 25 | 26 | /// A function that builds a widget tree based on the current [ChatViewModel]. 27 | /// 28 | /// This function is called with the current [BuildContext], the 29 | /// [ChatViewModel] obtained from the nearest [ChatViewModelProvider] 30 | /// ancestor, and the optional [child]. 31 | final Widget Function( 32 | BuildContext context, ChatViewModel viewModel, Widget? child) 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) => context 47 | .dependOnInheritedWidgetOfExactType() 48 | ?.viewModel; 49 | 50 | @override 51 | bool updateShouldNotify(ChatViewModelProvider oldWidget) => 52 | viewModel != oldWidget.viewModel; 53 | } 54 | -------------------------------------------------------------------------------- /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, showCupertinoDialog; 7 | import 'package:flutter/material.dart' show AlertDialog, 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 | /// Shows an adaptive dialog with the given [content] widget as content. 17 | /// 18 | /// This method automatically chooses between a Cupertino-style dialog for iOS 19 | /// and a Material-style dialog for other platforms. 20 | /// 21 | /// Parameters: 22 | /// * [context]: The build context in which to show the dialog. 23 | /// * [child]: The widget to display as the dialog's content. 24 | /// 25 | /// Returns a [Future] that completes with the result value when the dialog is 26 | /// dismissed. 27 | static Future show({ 28 | required BuildContext context, 29 | required Widget content, 30 | bool barrierDismissible = false, 31 | List actions = const [], 32 | }) => 33 | isCupertinoApp(context) 34 | ? showCupertinoDialog( 35 | context: context, 36 | barrierDismissible: barrierDismissible, 37 | builder: (context) => CupertinoAlertDialog( 38 | content: content, 39 | actions: actions, 40 | ), 41 | ) 42 | : showDialog( 43 | context: context, 44 | barrierDismissible: barrierDismissible, 45 | builder: (context) => AlertDialog( 46 | content: content, 47 | actions: actions, 48 | ), 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/dialogs/adaptive_dialog_action.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 CupertinoDialogAction; 6 | import 'package:flutter/material.dart' show TextButton; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | import '../utility.dart'; 10 | 11 | /// A button that adapts its appearance based on the design language, either 12 | /// Material or Cupertino. 13 | /// 14 | /// The [AdaptiveDialogAction] widget is designed to provide a consistent user 15 | /// experience across different platforms while adhering to platform-specific 16 | /// design guidelines. 17 | @immutable 18 | class AdaptiveDialogAction extends StatelessWidget { 19 | /// Creates an adaptive dialog action. 20 | /// 21 | /// The [onPressed] and [child] arguments must not be null. 22 | const AdaptiveDialogAction({ 23 | required this.onPressed, 24 | required this.child, 25 | super.key, 26 | }); 27 | 28 | /// The callback that is called when the button is tapped or pressed. 29 | final VoidCallback onPressed; 30 | 31 | /// The widget below this widget in the tree. 32 | /// 33 | /// Typically a [Text] widget. 34 | final Widget child; 35 | 36 | @override 37 | Widget build(BuildContext context) => isCupertinoApp(context) 38 | ? CupertinoDialogAction( 39 | onPressed: onPressed, 40 | child: child, 41 | ) 42 | : TextButton(onPressed: onPressed, child: child); 43 | } 44 | -------------------------------------------------------------------------------- /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(context) 31 | .showSnackBar(SnackBar(content: Text(message))); 32 | } 33 | } 34 | 35 | static void _showCupertinoSnackBar({ 36 | required BuildContext context, 37 | required String message, 38 | int durationMillis = 4000, 39 | }) { 40 | const animationDurationMillis = 200; 41 | final overlayEntry = OverlayEntry( 42 | builder: (context) => CupertinoSnackBar( 43 | message: message, 44 | animationDurationMillis: animationDurationMillis, 45 | waitDurationMillis: durationMillis, 46 | ), 47 | ); 48 | Future.delayed( 49 | Duration(milliseconds: durationMillis + 2 * animationDurationMillis), 50 | overlayEntry.remove, 51 | ); 52 | Overlay.of(context).insert(overlayEntry); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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( 51 | Duration( 52 | milliseconds: widget.waitDurationMillis, 53 | ), 54 | () { 55 | if (mounted) { 56 | setState(() => show = false); 57 | } 58 | }, 59 | ); 60 | } 61 | 62 | @override 63 | Widget build(BuildContext context) => AnimatedPositioned( 64 | bottom: show ? 8.0 : -50.0, 65 | left: 8, 66 | right: 8, 67 | curve: show ? Curves.linearToEaseOut : Curves.easeInToLinear, 68 | duration: Duration(milliseconds: widget.animationDurationMillis), 69 | child: CupertinoPopupSurface( 70 | child: Padding( 71 | padding: const EdgeInsets.symmetric( 72 | horizontal: 8, 73 | vertical: 8, 74 | ), 75 | child: Text( 76 | widget.message, 77 | style: const TextStyle( 78 | fontSize: 14, 79 | color: CupertinoColors.secondaryLabel, 80 | ), 81 | textAlign: TextAlign.center, 82 | ), 83 | ), 84 | ), 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /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( 23 | child: Image.memory(attachment.bytes, fit: BoxFit.contain), 24 | ), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /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 = Stream Function( 74 | String prompt, { 75 | required Iterable attachments, 76 | }); 77 | -------------------------------------------------------------------------------- /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/gemini_provider.dart'; 7 | export 'implementations/vertex_provider.dart'; 8 | export 'interface/attachments.dart'; 9 | export 'interface/chat_message.dart'; 10 | export 'interface/llm_provider.dart'; 11 | export 'interface/message_origin.dart'; 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: 44 | ToolkitTextStyles.body2.copyWith(color: ToolkitColors.hintText), 45 | hintText: 'Ask me anything...', 46 | backgroundColor: ToolkitColors.containerBackground, 47 | decoration: BoxDecoration( 48 | color: ToolkitColors.containerBackground, 49 | border: Border.all(width: 1, color: ToolkitColors.outline), 50 | borderRadius: BorderRadius.circular(24), 51 | ), 52 | ); 53 | 54 | /// The text style for the input text box. 55 | final TextStyle? textStyle; 56 | 57 | /// The hint text style for the input text box. 58 | final TextStyle? hintStyle; 59 | 60 | /// The hint text for the input text box. 61 | final String? hintText; 62 | 63 | /// The background color of the input box. 64 | final Color? backgroundColor; 65 | 66 | /// The decoration of the input box. 67 | final Decoration? decoration; 68 | } 69 | -------------------------------------------------------------------------------- /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( 60 | borderRadius: BorderRadius.circular(12), 61 | ), 62 | ), 63 | icon: ToolkitIcons.attach_file, 64 | iconColor: ToolkitColors.darkIcon, 65 | iconDecoration: ShapeDecoration( 66 | color: ToolkitColors.fileAttachmentIconBackground, 67 | shape: RoundedRectangleBorder( 68 | borderRadius: BorderRadius.circular(8), 69 | ), 70 | ), 71 | filenameStyle: ToolkitTextStyles.filename, 72 | filetypeStyle: ToolkitTextStyles.filetype, 73 | ); 74 | 75 | /// The decoration for the file attachment container. 76 | final Decoration? decoration; 77 | 78 | /// The icon to display for the file attachment. 79 | final IconData? icon; 80 | 81 | /// The color of the file attachment icon. 82 | final Color? iconColor; 83 | 84 | /// The decoration for the file attachment icon container. 85 | final Decoration? iconDecoration; 86 | 87 | /// The text style for the filename. 88 | final TextStyle? filenameStyle; 89 | 90 | /// The text style for the filetype. 91 | final TextStyle? filetypeStyle; 92 | } 93 | -------------------------------------------------------------------------------- /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({ 18 | this.textStyle, 19 | this.decoration, 20 | }); 21 | 22 | /// Resolves the [SuggestionStyle] by merging the provided [style] with the 23 | /// [defaultStyle]. 24 | /// 25 | /// If [style] is null, the [defaultStyle] is used. If [defaultStyle] is not 26 | /// provided, the [defaultStyle] is obtained from 27 | /// [SuggestionStyle.defaultStyle]. 28 | factory SuggestionStyle.resolve( 29 | SuggestionStyle? style, { 30 | SuggestionStyle? defaultStyle, 31 | }) { 32 | defaultStyle ??= SuggestionStyle.defaultStyle(); 33 | return SuggestionStyle( 34 | textStyle: style?.textStyle ?? defaultStyle.textStyle, 35 | decoration: style?.decoration ?? defaultStyle.decoration, 36 | ); 37 | } 38 | 39 | /// Provides a default style. 40 | /// 41 | /// This style is typically used as the base style for suggestions. 42 | factory SuggestionStyle.defaultStyle() => SuggestionStyle._lightStyle(); 43 | 44 | /// Provides a default light style. 45 | /// 46 | /// This style is typically used for suggestions in light mode. 47 | factory SuggestionStyle._lightStyle() => SuggestionStyle( 48 | textStyle: ToolkitTextStyles.body1, 49 | decoration: const BoxDecoration( 50 | color: ToolkitColors.userMessageBackground, 51 | borderRadius: BorderRadius.all(Radius.circular(8)), 52 | ), 53 | ); 54 | 55 | /// The text style for the suggestion. 56 | final TextStyle? textStyle; 57 | 58 | /// The decoration for the suggestion. 59 | final Decoration? decoration; 60 | } 61 | -------------------------------------------------------------------------------- /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 = 29 | IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); 30 | 31 | /// Icon representing a spark or idea. 32 | static const IconData spark_icon = 33 | IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); 34 | 35 | /// Icon for adding or creating new items. 36 | static const IconData add = 37 | IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); 38 | 39 | /// Icon for attaching files. 40 | static const IconData attach_file = 41 | IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); 42 | 43 | /// Icon for stopping or halting an action. 44 | static const IconData stop = 45 | IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); 46 | 47 | /// Icon representing a microphone. 48 | static const IconData mic = 49 | IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); 50 | 51 | /// Icon for closing or dismissing. 52 | static const IconData close = 53 | IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); 54 | 55 | /// Icon representing a camera. 56 | static const IconData camera_alt = 57 | IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); 58 | 59 | /// Icon representing an image or picture. 60 | static const IconData image = 61 | IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg); 62 | 63 | /// Icon for editing. 64 | static const IconData edit = 65 | IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); 66 | 67 | /// Icon for copying content. 68 | static const IconData content_copy = 69 | IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); 70 | } 71 | -------------------------------------------------------------------------------- /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.withOpacity(0.9), 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({ 15 | this.textStyle, 16 | this.decoration, 17 | }); 18 | 19 | /// Resolves the UserMessageStyle by combining the provided style with default 20 | /// values. 21 | /// 22 | /// This method takes an optional [style] and merges it with the 23 | /// [defaultStyle]. If [defaultStyle] is not provided, it uses 24 | /// [UserMessageStyle.defaultStyle]. 25 | /// 26 | /// [style] - The custom UserMessageStyle to apply. Can be null. 27 | /// [defaultStyle] - The default UserMessageStyle to use as a base. If null, 28 | /// uses [UserMessageStyle.defaultStyle]. 29 | /// 30 | /// Returns a new [UserMessageStyle] instance with resolved properties. 31 | factory UserMessageStyle.resolve( 32 | UserMessageStyle? style, { 33 | UserMessageStyle? defaultStyle, 34 | }) { 35 | defaultStyle ??= UserMessageStyle.defaultStyle(); 36 | return UserMessageStyle( 37 | textStyle: style?.textStyle ?? defaultStyle.textStyle, 38 | decoration: style?.decoration ?? defaultStyle.decoration, 39 | ); 40 | } 41 | 42 | /// Provides default style data for user messages. 43 | factory UserMessageStyle.defaultStyle() => UserMessageStyle._lightStyle(); 44 | 45 | /// Provides a default light style. 46 | factory UserMessageStyle._lightStyle() => UserMessageStyle( 47 | textStyle: ToolkitTextStyles.body1, 48 | decoration: const BoxDecoration( 49 | color: ToolkitColors.userMessageBackground, 50 | borderRadius: BorderRadius.only( 51 | topLeft: Radius.circular(20), 52 | topRight: Radius.zero, 53 | bottomLeft: Radius.circular(20), 54 | bottomRight: Radius.circular(20), 55 | ), 56 | ), 57 | ); 58 | 59 | /// The text style for user messages. 60 | final TextStyle? textStyle; 61 | 62 | /// The decoration for user message bubbles. 63 | final Decoration? decoration; 64 | } 65 | -------------------------------------------------------------------------------- /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) => Color.fromARGB( 70 | color!.alpha, 71 | 255 - color.red, 72 | 255 - color.green, 73 | 255 - color.blue, 74 | ); 75 | -------------------------------------------------------------------------------- /lib/src/views/action_button/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: isCupertinoApp(context) 48 | ? Icon( 49 | style.icon, 50 | color: style.iconColor, 51 | size: size * 0.6, 52 | ) 53 | : Tooltip( 54 | message: style.tooltip, 55 | textStyle: style.tooltipTextStyle, 56 | decoration: style.tooltipDecoration, 57 | child: Icon( 58 | style.icon, 59 | color: style.iconColor, 60 | size: size * 0.6, 61 | ), 62 | ), 63 | ), 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/views/action_button/action_button_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/widgets.dart'; 6 | 7 | import '../../styles/styles.dart'; 8 | import 'action_button.dart'; 9 | 10 | /// A widget that displays a horizontal bar of [ActionButton]s. 11 | /// 12 | /// This widget creates a container with rounded corners that houses a series of 13 | /// [ActionButton]s. The buttons are laid out horizontally and can overflow if 14 | /// there's not enough space. 15 | @immutable 16 | class ActionButtonBar extends StatelessWidget { 17 | /// Creates a [ActionButtonBar]. 18 | /// 19 | /// The [buttons] parameter is required and specifies the list of 20 | /// [ActionButton]s to be displayed in the bar. 21 | const ActionButtonBar( 22 | this.buttons, { 23 | required this.style, 24 | super.key, 25 | }); 26 | 27 | /// The list of [ActionButton]s to be displayed in the bar. 28 | final List buttons; 29 | 30 | /// The style of the action button bar. 31 | final LlmChatViewStyle style; 32 | 33 | @override 34 | Widget build(BuildContext context) => DecoratedBox( 35 | decoration: style.actionButtonBarDecoration!, 36 | child: OverflowBar( 37 | children: buttons, 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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) => isCupertinoApp(context) 29 | ? CupertinoActivityIndicator(color: color) 30 | : CircularProgressIndicator(color: color); 31 | } 32 | -------------------------------------------------------------------------------- /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 | import 'package:gap/gap.dart'; 7 | 8 | import '../../chat_view_model/chat_view_model_client.dart'; 9 | import '../../providers/interface/attachments.dart'; 10 | import '../../styles/file_attachment_style.dart'; 11 | 12 | /// A widget that displays a file attachment. 13 | /// 14 | /// This widget creates a container with a file icon and information about the 15 | /// attached file, such as its name and MIME type. 16 | @immutable 17 | class FileAttachmentView extends StatelessWidget { 18 | /// Creates a FileAttachmentView. 19 | /// 20 | /// The [attachment] parameter must not be null and represents the 21 | /// file attachment to be displayed. 22 | const FileAttachmentView(this.attachment, {super.key}); 23 | 24 | /// The file attachment to be displayed. 25 | final FileAttachment attachment; 26 | 27 | @override 28 | Widget build(BuildContext context) => ChatViewModelClient( 29 | builder: (context, viewModel, child) { 30 | final attachmentStyle = FileAttachmentStyle.resolve( 31 | viewModel.style?.fileAttachmentStyle, 32 | ); 33 | 34 | return Container( 35 | height: 80, 36 | padding: const EdgeInsets.all(8), 37 | decoration: attachmentStyle.decoration, 38 | child: Row( 39 | mainAxisSize: MainAxisSize.min, 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 | const Gap(8), 52 | Flexible( 53 | child: Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | mainAxisAlignment: MainAxisAlignment.center, 56 | children: [ 57 | Text( 58 | attachment.name, 59 | style: attachmentStyle.filenameStyle, 60 | overflow: TextOverflow.ellipsis, 61 | ), 62 | Text( 63 | attachment.mimeType, 64 | style: attachmentStyle.filetypeStyle, 65 | overflow: TextOverflow.ellipsis, 66 | ), 67 | ], 68 | ), 69 | ), 70 | ], 71 | ), 72 | ); 73 | }, 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /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 | Future _showPreviewDialog(BuildContext context) async => 37 | AdaptiveAlertDialog.show( 38 | context: context, 39 | barrierDismissible: true, 40 | content: ImagePreviewDialog(attachment), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/views/chat_history_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 '../providers/interface/message_origin.dart'; 10 | import 'chat_message_view/llm_message_view.dart'; 11 | import 'chat_message_view/user_message_view.dart'; 12 | 13 | /// A widget that displays a history of chat messages. 14 | /// 15 | /// This widget renders a scrollable list of chat messages, supporting 16 | /// selection and editing of messages. It displays messages in reverse 17 | /// chronological order (newest at the bottom). 18 | @immutable 19 | class ChatHistoryView extends StatefulWidget { 20 | /// Creates a [ChatHistoryView]. 21 | /// 22 | /// If [onEditMessage] is provided, it will be called when a user initiates an 23 | /// edit action on an editable message (typically the last user message in the 24 | /// history). 25 | const ChatHistoryView({ 26 | this.onEditMessage, 27 | super.key, 28 | }); 29 | 30 | /// Optional callback function for editing a message. 31 | /// 32 | /// If provided, this function will be called when a user initiates an edit 33 | /// action on an editable message (typically the last user message in the 34 | /// history). The function receives the [ChatMessage] to be edited as its 35 | /// parameter. 36 | final void Function(ChatMessage message)? onEditMessage; 37 | 38 | @override 39 | State createState() => _ChatHistoryViewState(); 40 | } 41 | 42 | class _ChatHistoryViewState extends State { 43 | @override 44 | Widget build(BuildContext context) => Padding( 45 | padding: const EdgeInsets.only(top: 16, left: 16, right: 16), 46 | child: ChatViewModelClient( 47 | builder: (context, viewModel, child) { 48 | final history = [ 49 | if (viewModel.welcomeMessage != null) 50 | ChatMessage( 51 | origin: MessageOrigin.llm, 52 | text: viewModel.welcomeMessage, 53 | attachments: [], 54 | ), 55 | ...viewModel.provider.history, 56 | ]; 57 | 58 | return ListView.builder( 59 | reverse: true, 60 | itemCount: history.length, 61 | itemBuilder: (context, index) { 62 | final messageIndex = history.length - index - 1; 63 | final message = history[messageIndex]; 64 | final isLastUserMessage = 65 | message.origin.isUser && messageIndex >= history.length - 2; 66 | final canEdit = 67 | isLastUserMessage && widget.onEditMessage != null; 68 | final isUser = message.origin.isUser; 69 | 70 | return Padding( 71 | padding: const EdgeInsets.only(top: 6), 72 | child: isUser 73 | ? UserMessageView( 74 | message, 75 | onEdit: canEdit 76 | ? () => widget.onEditMessage?.call(message) 77 | : null, 78 | ) 79 | : LlmMessageView( 80 | message, 81 | isWelcomeMessage: messageIndex == 0, 82 | ), 83 | ); 84 | }, 85 | ); 86 | }, 87 | ), 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /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: attachments.isNotEmpty 38 | ? ListView( 39 | scrollDirection: Axis.horizontal, 40 | children: [ 41 | for (final a in attachments) 42 | RemovableAttachment(attachment: a, onRemove: onRemove), 43 | ], 44 | ) 45 | : const SizedBox(), 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /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: Padding( 42 | padding: const EdgeInsets.all(8), 43 | child: Container( 44 | padding: const EdgeInsets.all(8), 45 | decoration: suggestionStyle.decoration, 46 | child: Text( 47 | suggestion, 48 | softWrap: true, 49 | maxLines: 3, 50 | style: suggestionStyle.textStyle, 51 | ), 52 | ), 53 | ), 54 | ), 55 | ], 56 | ); 57 | }, 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /lib/src/views/chat_input/editing_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:gap/gap.dart'; 3 | 4 | import '../../styles/action_button_style.dart'; 5 | import '../../styles/toolkit_text_styles.dart'; 6 | import '../../utility.dart'; 7 | import '../action_button/action_button.dart'; 8 | 9 | /// A widget that displays an editing indicator with a cancel button. 10 | /// 11 | /// This widget is used to show that the user is currently editing a message. 12 | /// It provides a visual indicator with the text "Editing" and a button to 13 | /// cancel the editing action. 14 | /// 15 | /// The [onCancelEdit] callback is triggered when the cancel button is pressed. 16 | /// The [cancelButtonStyle] is used to style the cancel button. 17 | class EditingIndicator extends StatelessWidget { 18 | /// Creates an [EditingIndicator]. 19 | /// 20 | /// The [onCancelEdit] and [cancelButtonStyle] parameters are required. 21 | const EditingIndicator({ 22 | required this.onCancelEdit, 23 | required this.cancelButtonStyle, 24 | super.key, 25 | }); 26 | 27 | /// The callback to be invoked when the cancel button is pressed. 28 | final VoidCallback onCancelEdit; 29 | 30 | /// The style to be applied to the cancel button. 31 | final ActionButtonStyle cancelButtonStyle; 32 | 33 | @override 34 | Widget build(BuildContext context) => Padding( 35 | padding: const EdgeInsets.only(right: 16), 36 | child: Row( 37 | mainAxisAlignment: MainAxisAlignment.end, 38 | crossAxisAlignment: CrossAxisAlignment.end, 39 | children: [ 40 | Text( 41 | 'Editing', 42 | style: ToolkitTextStyles.label.copyWith( 43 | color: invertColor(cancelButtonStyle.iconColor), 44 | ), 45 | ), 46 | const Gap(6), 47 | ActionButton( 48 | onPressed: onCancelEdit, 49 | style: cancelButtonStyle, 50 | size: 16, 51 | ), 52 | ], 53 | ), 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /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/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 | }; 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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: attachment is ImageFileAttachment 44 | ? () => unawaited(_showPreviewDialog(context)) 45 | : null, 46 | child: Container( 47 | padding: const EdgeInsets.only(right: 12), 48 | height: 80, 49 | child: AttachmentView(attachment), 50 | ), 51 | ), 52 | Padding( 53 | padding: const EdgeInsets.all(2), 54 | child: ChatViewModelClient( 55 | builder: (context, viewModel, child) { 56 | final chatStyle = LlmChatViewStyle.resolve(viewModel.style); 57 | return ActionButton( 58 | style: chatStyle.closeButtonStyle!, 59 | size: 20, 60 | onPressed: () => onRemove(attachment), 61 | ); 62 | }, 63 | ), 64 | ), 65 | ], 66 | ); 67 | 68 | Future _showPreviewDialog(BuildContext context) async => 69 | AdaptiveAlertDialog.show( 70 | context: context, 71 | barrierDismissible: true, 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/material.dart' 4 | show DefaultMaterialLocalizations, SelectionArea; 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:flutter_context_menu/flutter_context_menu.dart'; 7 | 8 | import '../../styles/llm_chat_view_style.dart'; 9 | import '../../utility.dart'; 10 | 11 | /// A widget that displays text with adaptive copy functionality. 12 | /// 13 | /// This widget provides a context menu for copying text to the clipboard on 14 | /// mobile devices, and a selection area for mouse-driven selection on desktop 15 | /// and web platforms. 16 | @immutable 17 | class AdaptiveCopyText extends StatelessWidget { 18 | /// Creates an [AdaptiveCopyText] widget. 19 | /// 20 | /// The [clipboardText] parameter is required and contains the text to be 21 | /// copied to the clipboard. The [child] parameter is required and contains 22 | /// the widget to be displayed. The [chatStyle] parameter is required and 23 | /// contains the style information for the chat. The [onEdit] parameter is 24 | /// optional and contains the callback to be invoked when the text is edited. 25 | const AdaptiveCopyText({ 26 | required this.clipboardText, 27 | required this.child, 28 | required this.chatStyle, 29 | this.onEdit, 30 | super.key, 31 | }); 32 | 33 | /// The text to be copied to the clipboard. 34 | final String clipboardText; 35 | 36 | /// The widget to be displayed. 37 | final Widget child; 38 | 39 | /// The callback to be invoked when the text is edited. 40 | final VoidCallback? onEdit; 41 | 42 | /// The style information for the chat. 43 | final LlmChatViewStyle chatStyle; 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | final contextMenu = ContextMenu( 48 | entries: [ 49 | if (onEdit != null) 50 | MenuItem( 51 | label: 'Edit', 52 | icon: chatStyle.editButtonStyle!.icon, 53 | onSelected: onEdit, 54 | ), 55 | MenuItem( 56 | label: 'Copy', 57 | icon: chatStyle.copyButtonStyle!.icon, 58 | onSelected: () => unawaited(copyToClipboard(context, clipboardText)), 59 | ), 60 | ], 61 | ); 62 | 63 | // On mobile, show the context menu for long-press; 64 | // on desktop and web, show the selection area for mouse-driven selection. 65 | return isMobile 66 | ? ContextMenuRegion(contextMenu: contextMenu, child: child) 67 | : Localizations( 68 | locale: Localizations.localeOf(context), 69 | delegates: const [ 70 | DefaultWidgetsLocalizations.delegate, 71 | DefaultMaterialLocalizations.delegate, 72 | ], 73 | child: SelectionArea(child: child), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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) => isCupertinoApp(context) 73 | ? CupertinoTextField( 74 | minLines: minLines, 75 | maxLines: maxLines, 76 | controller: controller, 77 | autofocus: autofocus, 78 | focusNode: focusNode, 79 | onSubmitted: onSubmitted, 80 | style: style, 81 | placeholder: hintText, 82 | placeholderStyle: hintStyle, 83 | padding: hintPadding ?? EdgeInsets.zero, 84 | decoration: BoxDecoration( 85 | border: Border.all(width: 0, color: ToolkitColors.transparent), 86 | ), 87 | textInputAction: textInputAction, 88 | ) 89 | : TextField( 90 | minLines: minLines, 91 | maxLines: maxLines, 92 | controller: controller, 93 | autofocus: autofocus, 94 | focusNode: focusNode, 95 | textInputAction: textInputAction, 96 | onSubmitted: onSubmitted, 97 | style: style, 98 | decoration: InputDecoration( 99 | border: InputBorder.none, 100 | hintText: hintText, 101 | hintStyle: hintStyle, 102 | contentPadding: hintPadding, 103 | ), 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /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 = Widget Function( 18 | BuildContext context, 19 | String response, 20 | ); 21 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_ai_toolkit 2 | description: "A set of AI chat-related widgets for your Flutter app targeting mobile, desktop and web." 3 | version: 0.6.5 4 | homepage: https://github.com/csells/flutter_ai_toolkit 5 | 6 | environment: 7 | sdk: '>=3.4.0 <4.0.0' 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | cross_file: ^0.3.4+2 12 | file_selector: ^1.0.3 13 | firebase_vertexai: ^1.0.1 14 | flutter: 15 | sdk: flutter 16 | flutter_context_menu: ^0.2.0 17 | flutter_markdown: ^0.7.4+3 18 | flutter_picture_taker: ^0.2.0 19 | gap: ^3.0.1 20 | google_fonts: ^6.2.1 21 | google_generative_ai: ^0.4.3 22 | image_picker: ^1.1.2 23 | mime: ^2.0.0 24 | universal_platform: ^1.1.0 25 | uuid: ^4.4.2 26 | waveform_recorder: ^1.3.0 27 | 28 | dev_dependencies: 29 | all_lint_rules_community: ^0.0.42 30 | flutter_test: 31 | sdk: flutter 32 | 33 | flutter: 34 | fonts: 35 | - family: FatIcons 36 | fonts: 37 | - asset: lib/fonts/FatIcons.ttf 38 | --------------------------------------------------------------------------------