├── .github └── workflows │ ├── android.yml │ └── ios.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ ├── actual.kt │ │ └── dev │ │ │ └── johnoreilly │ │ │ └── gemini │ │ │ ├── AndroidJsonDatabase.kt │ │ │ ├── AndroidTextToSpeech.kt │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── strings.xml │ ├── commonMain │ ├── composeResources │ │ └── drawable │ │ │ ├── assistant.png │ │ │ ├── chat.png │ │ │ ├── copy.png │ │ │ ├── dust.png │ │ │ ├── moon.png │ │ │ ├── rotate_right.xml │ │ │ ├── sound.png │ │ │ └── sun.png │ └── kotlin │ │ ├── App.kt │ │ ├── GeminiApi.kt │ │ ├── core │ │ ├── app_db │ │ │ ├── ChatDbManager.kt │ │ │ └── DataStoreManager.kt │ │ └── models │ │ │ └── ChatMessage.kt │ │ ├── expect.kt │ │ ├── ui │ │ ├── screens │ │ │ └── gemini_ai │ │ │ │ ├── Assistant.kt │ │ │ │ ├── Chat.kt │ │ │ │ ├── GeminiAIScreen.kt │ │ │ │ └── GeminiAiScreenModel.kt │ │ ├── theme │ │ │ └── app_theme.kt │ │ └── widgets.kt │ │ └── utils │ │ ├── AppConstants.kt │ │ └── ConnectionState.kt │ ├── desktopMain │ └── kotlin │ │ ├── DeskTopJsonDB.kt │ │ ├── DesktopTextToSpeech.kt │ │ ├── actual.kt │ │ └── main.kt │ ├── iosMain │ └── kotlin │ │ ├── IosJsonDB.kt │ │ ├── IosTextToSpeech.kt │ │ ├── MainViewController.kt │ │ └── actual.kt │ └── wasmJsMain │ ├── kotlin │ ├── WebJsonDB.kt │ ├── WebTextToSpeech.kt │ ├── actual.kt │ └── main.kt │ └── resources │ └── index.html ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── joreilly.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── joreilly.xcuserdatad │ │ └── xcschemes │ │ ├── iosApp.xcscheme │ │ └── xcschememanagement.plist └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── settings.gradle.kts └── wearApp ├── build.gradle.kts └── src └── main ├── AndroidManifest.xml ├── kotlin └── dev │ └── johnoreilly │ └── gemini │ ├── common │ └── GeminiApi.kt │ └── wear │ ├── MainActivity.kt │ ├── Screen.kt │ ├── WearApp.kt │ ├── markdown │ └── WearMaterialTypography.kt │ └── prompt │ ├── GeminiPromptScreen.kt │ └── GeminiPromptViewModel.kt └── res ├── drawable-v24 └── ic_launcher_foreground.xml ├── drawable └── ic_launcher_background.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-mdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxxhdpi ├── ic_launcher.png └── ic_launcher_round.png └── values └── strings.xml /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: set up JDK 17 13 | uses: actions/setup-java@v4 14 | with: 15 | distribution: 'zulu' 16 | java-version: 17 17 | - name: Build android app 18 | run: ./gradlew assembleDebug 19 | - name: Run Unit Tests 20 | run: ./gradlew test 21 | - name: Build iOS shared code 22 | run: ./gradlew :composeApp:compileKotlinIosSimulatorArm64 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | name: iOS CI 2 | 3 | on: pull_request 4 | 5 | # Cancel any current or previous job from the same PR 6 | concurrency: 7 | group: ios-${{ github.head_ref }} 8 | cancel-in-progress: true 9 | 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-14 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-java@v4 17 | with: 18 | distribution: 'zulu' 19 | java-version: 17 20 | 21 | - name: Build iOS app 22 | run: xcodebuild -allowProvisioningUpdates -workspace iosApp/iosApp.xcodeproj/project.xcworkspace -configuration Debug -scheme iosApp -sdk iphoneos -destination name='iPhone 15' 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | /local.properties 8 | **/.DS_Store 9 | **/build/ 10 | .gradle 11 | **/.idea/* 12 | .kotlin 13 | yarn.lock 14 | *.xcworkspacedata 15 | 16 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,database,androidstudio,intellij+all,java 17 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,database,androidstudio,intellij+all,java 18 | 19 | ### Database ### 20 | *.accdb 21 | *.db 22 | *.dbf 23 | *.mdb 24 | *.pdb 25 | *.sqlite3 26 | *.db-shm 27 | *.db-wal 28 | 29 | ### Intellij+all ### 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 31 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 32 | 33 | # User-specific stuff 34 | .idea/**/workspace.xml 35 | .idea/**/tasks.xml 36 | .idea/**/usage.statistics.xml 37 | .idea/**/dictionaries 38 | .idea/**/shelf 39 | 40 | # AWS User-specific 41 | .idea/**/aws.xml 42 | 43 | # Generated files 44 | .idea/**/contentModel.xml 45 | 46 | # Sensitive or high-churn files 47 | .idea/**/dataSources/ 48 | .idea/**/dataSources.ids 49 | .idea/**/dataSources.local.xml 50 | .idea/**/sqlDataSources.xml 51 | .idea/**/dynamic.xml 52 | .idea/**/uiDesigner.xml 53 | .idea/**/dbnavigator.xml 54 | 55 | # Gradle 56 | .idea/**/gradle.xml 57 | .idea/**/libraries 58 | 59 | # Gradle and Maven with auto-import 60 | # When using Gradle or Maven with auto-import, you should exclude module files, 61 | # since they will be recreated, and may cause churn. Uncomment if using 62 | # auto-import. 63 | # .idea/artifacts 64 | # .idea/compiler.xml 65 | # .idea/jarRepositories.xml 66 | # .idea/modules.xml 67 | # .idea/*.iml 68 | # .idea/modules 69 | # *.iml 70 | # *.ipr 71 | 72 | # CMake 73 | cmake-build-*/ 74 | 75 | # Mongo Explorer plugin 76 | .idea/**/mongoSettings.xml 77 | 78 | # File-based project format 79 | *.iws 80 | 81 | # IntelliJ 82 | out/ 83 | 84 | # mpeltonen/sbt-idea plugin 85 | .idea_modules/ 86 | 87 | # JIRA plugin 88 | atlassian-ide-plugin.xml 89 | 90 | # Cursive Clojure plugin 91 | .idea/replstate.xml 92 | 93 | # SonarLint plugin 94 | .idea/sonarlint/ 95 | 96 | # Crashlytics plugin (for Android Studio and IntelliJ) 97 | com_crashlytics_export_strings.xml 98 | crashlytics.properties 99 | crashlytics-build.properties 100 | fabric.properties 101 | 102 | # Editor-based Rest Client 103 | .idea/httpRequests 104 | 105 | # Android studio 3.1+ serialized cache file 106 | .idea/caches/build_file_checksums.ser 107 | 108 | ### Intellij+all Patch ### 109 | # Ignore everything but code style settings and run configurations 110 | # that are supposed to be shared within teams. 111 | 112 | .idea/* 113 | 114 | !.idea/codeStyles 115 | !.idea/runConfigurations 116 | 117 | ### Java ### 118 | # Compiled class file 119 | *.class 120 | 121 | # Log file 122 | *.log 123 | 124 | # BlueJ files 125 | *.ctxt 126 | 127 | # Mobile Tools for Java (J2ME) 128 | .mtj.tmp/ 129 | 130 | # Package Files # 131 | *.jar 132 | *.war 133 | *.nar 134 | *.ear 135 | *.zip 136 | *.tar.gz 137 | *.rar 138 | 139 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 140 | hs_err_pid* 141 | replay_pid* 142 | 143 | ### Kotlin ### 144 | # Compiled class file 145 | 146 | # Log file 147 | 148 | # BlueJ files 149 | 150 | # Mobile Tools for Java (J2ME) 151 | 152 | # Package Files # 153 | 154 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 155 | 156 | ### AndroidStudio ### 157 | # Covers files to be ignored for android development using Android Studio. 158 | 159 | # Built application files 160 | *.apk 161 | *.ap_ 162 | *.aab 163 | 164 | # Files for the ART/Dalvik VM 165 | *.dex 166 | 167 | # Java class files 168 | 169 | # Generated files 170 | bin/ 171 | gen/ 172 | 173 | # Gradle files 174 | .gradle 175 | .gradle/ 176 | build/ 177 | 178 | # Signing files 179 | .signing/ 180 | 181 | # Local configuration file (sdk path, etc) 182 | local.properties 183 | 184 | # Proguard folder generated by Eclipse 185 | proguard/ 186 | 187 | # Log Files 188 | 189 | # Android Studio 190 | /*/build/ 191 | /*/local.properties 192 | /*/out 193 | /*/*/build 194 | /*/*/production 195 | captures/ 196 | .navigation/ 197 | *.ipr 198 | *~ 199 | *.swp 200 | 201 | # Keystore files 202 | *.jks 203 | *.keystore 204 | 205 | # Google Services (e.g. APIs or Firebase) 206 | # google-services.json 207 | 208 | # Android Patch 209 | gen-external-apklibs 210 | 211 | # External native build folder generated in Android Studio 2.2 and later 212 | .externalNativeBuild 213 | 214 | # NDK 215 | obj/ 216 | 217 | # IntelliJ IDEA 218 | *.iml 219 | /out/ 220 | 221 | # User-specific configurations 222 | .idea/caches/ 223 | .idea/libraries/ 224 | .idea/shelf/ 225 | .idea/workspace.xml 226 | .idea/tasks.xml 227 | .idea/.name 228 | .idea/compiler.xml 229 | .idea/copyright/profiles_settings.xml 230 | .idea/encodings.xml 231 | .idea/misc.xml 232 | .idea/modules.xml 233 | .idea/scopes/scope_settings.xml 234 | .idea/dictionaries 235 | .idea/vcs.xml 236 | .idea/jsLibraryMappings.xml 237 | .idea/datasources.xml 238 | .idea/dataSources.ids 239 | .idea/sqlDataSources.xml 240 | .idea/dynamic.xml 241 | .idea/uiDesigner.xml 242 | .idea/assetWizardSettings.xml 243 | .idea/gradle.xml 244 | .idea/jarRepositories.xml 245 | .idea/navEditor.xml 246 | 247 | # Legacy Eclipse project files 248 | .classpath 249 | .project 250 | .cproject 251 | .settings/ 252 | 253 | # Mobile Tools for Java (J2ME) 254 | 255 | # Package Files # 256 | 257 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 258 | 259 | ## Plugin-specific files: 260 | 261 | # mpeltonen/sbt-idea plugin 262 | 263 | # JIRA plugin 264 | 265 | # Mongo Explorer plugin 266 | .idea/mongoSettings.xml 267 | 268 | # Crashlytics plugin (for Android Studio and IntelliJ) 269 | 270 | ### AndroidStudio Patch ### 271 | 272 | !/gradle/wrapper/gradle-wrapper.jar 273 | 274 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,database,androidstudio,intellij+all,java -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![kotlin-version](https://img.shields.io/badge/kotlin-2.1.20-blue?logo=kotlin) 2 | 3 | Kotlin/Compose Multiplatform sample to demonstrate Gemini Generative AI APIs (text and image based queries). 4 | Uses [Generative AI SDK](https://github.com/PatilShreyas/generative-ai-kmp). 5 | 6 | 7 | Running on 8 | * iOS 9 | * Android 10 | * Wear OS (contributed by https://github.com/yschimke) 11 | * Desktop 12 | * Web (Wasm) 13 | 14 | Set your Gemini API key (`gemini_api_key`) in `local.properties` 15 | 16 | Related posts: 17 | * [Exploring use of Gemini Generative AI APIs in a Kotlin/Compose Multiplatform project](https://johnoreilly.dev/posts/gemini-kotlin-multiplatform/) 18 | 19 | 20 | 21 | ## Screenshots 22 | 23 | ### iOS 24 | 25 | ![Simulator Screenshot - iPhone 15 Pro - 2024-01-19 at 19 15 53](https://github.com/joreilly/GeminiKMP/assets/6302/91e5d4f5-7cb5-40d4-95fb-c5d87bac7918) 26 | 27 | 28 | ### Android 29 | 30 | 31 | ![Screenshot_1705691519](https://github.com/joreilly/GeminiKMP/assets/6302/668145c1-1dcf-4cd5-8b1d-a04f7ebd6866) 32 | 33 | 34 | ### Compose for Desktop 35 | 36 | Screenshot 2024-01-19 at 19 03 52 37 | 38 | 39 | Screenshot 2024-01-14 at 17 41 26 40 | 41 | 42 | 43 | 44 | 45 | ### Wasm based Compose for Web 46 | 47 | Screenshot 2023-12-31 at 13 01 02 48 | 49 | Screenshot 2024-01-14 at 19 26 05 50 | 51 | ## Full set of Kotlin Multiplatform/Compose/SwiftUI samples 52 | 53 | * PeopleInSpace (https://github.com/joreilly/PeopleInSpace) 54 | * GalwayBus (https://github.com/joreilly/GalwayBus) 55 | * Confetti (https://github.com/joreilly/Confetti) 56 | * BikeShare (https://github.com/joreilly/BikeShare) 57 | * FantasyPremierLeague (https://github.com/joreilly/FantasyPremierLeague) 58 | * ClimateTrace (https://github.com/joreilly/ClimateTraceKMP) 59 | * GeminiKMP (https://github.com/joreilly/GeminiKMP) 60 | * MortyComposeKMM (https://github.com/joreilly/MortyComposeKMM) 61 | * StarWars (https://github.com/joreilly/StarWars) 62 | * WordMasterKMP (https://github.com/joreilly/WordMasterKMP) 63 | * Chip-8 (https://github.com/joreilly/chip-8) 64 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) apply false 3 | alias(libs.plugins.androidLibrary) apply false 4 | alias(libs.plugins.jetbrainsCompose) apply false 5 | alias(libs.plugins.kotlinMultiplatform) apply false 6 | } -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.codingfeline.buildkonfig.compiler.FieldSpec 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 4 | import java.util.Properties 5 | 6 | plugins { 7 | alias(libs.plugins.kotlinMultiplatform) 8 | alias(libs.plugins.androidApplication) 9 | alias(libs.plugins.jetbrainsCompose) 10 | alias(libs.plugins.compose.compiler) 11 | alias(libs.plugins.kotlinx.serialization) 12 | alias(libs.plugins.buildkonfig) 13 | } 14 | 15 | kotlin { 16 | 17 | androidTarget { 18 | compilations.all { 19 | kotlinOptions { 20 | jvmTarget = "1.8" 21 | } 22 | } 23 | } 24 | 25 | jvm("desktop") 26 | 27 | @OptIn(ExperimentalWasmDsl::class, org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) 28 | wasmJs { 29 | moduleName = "composeApp" 30 | browser { 31 | commonWebpackConfig { 32 | outputFileName = "composeApp.js" 33 | } 34 | } 35 | binaries.executable() 36 | } 37 | 38 | listOf( 39 | iosX64(), 40 | iosArm64(), 41 | iosSimulatorArm64() 42 | ).forEach { iosTarget -> 43 | iosTarget.binaries.framework { 44 | baseName = "ComposeApp" 45 | isStatic = true 46 | } 47 | } 48 | 49 | sourceSets { 50 | 51 | all { 52 | languageSettings { 53 | optIn("nl.marc_apps.tts.experimental.ExperimentalVoiceApi") 54 | optIn("nl.marc_apps.tts.experimental.ExperimentalDesktopTarget") 55 | } 56 | } 57 | 58 | commonMain.dependencies { 59 | implementation(compose.runtime) 60 | implementation(compose.foundation) 61 | implementation(compose.material3) 62 | implementation(compose.ui) 63 | implementation(compose.components.resources) 64 | implementation(compose.components.uiToolingPreview) 65 | 66 | implementation(libs.kotlinx.coroutines.core) 67 | 68 | implementation(libs.markdown.renderer) 69 | api(libs.compose.window.size) 70 | 71 | api(libs.generativeai) 72 | 73 | implementation(libs.filekit.dialogs.compose) 74 | implementation(libs.filekit.coil) 75 | // voyager is a multiplatform library for viewmodel navigation 76 | implementation(libs.voyager.navigator) 77 | implementation(libs.voyager.screenmodel) 78 | // storage data 79 | implementation(libs.multiplatform.settings) 80 | implementation(libs.multiplatform.settings.coroutines) 81 | // Time 82 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") 83 | implementation(libs.kotlinx.serialization.json) 84 | 85 | } 86 | androidMain.dependencies { 87 | implementation(libs.compose.ui.tooling.preview) 88 | implementation(libs.androidx.activity.compose) 89 | implementation(libs.kotlinx.coroutines.android) 90 | } 91 | 92 | val desktopMain by getting { 93 | dependencies { 94 | implementation(compose.desktop.currentOs) 95 | implementation(libs.kotlinx.coroutines.swing) 96 | implementation("nl.marc-apps:tts:2.5.0") 97 | } 98 | } 99 | 100 | iosMain.dependencies {} 101 | 102 | wasmJsMain.dependencies { 103 | implementation("nl.marc-apps:tts:2.5.0") 104 | } 105 | 106 | } 107 | } 108 | 109 | android { 110 | namespace = "dev.johnoreilly.gemini" 111 | compileSdk = 35 112 | 113 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 114 | sourceSets["main"].res.srcDirs("src/androidMain/res") 115 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 116 | 117 | defaultConfig { 118 | applicationId = "dev.johnoreilly.gemini" 119 | minSdk = libs.versions.android.minSdk.get().toInt() 120 | targetSdk = libs.versions.android.targetSdk.get().toInt() 121 | versionCode = 1 122 | versionName = "1.0" 123 | } 124 | packaging { 125 | resources { 126 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 127 | } 128 | } 129 | buildTypes { 130 | getByName("release") { 131 | isMinifyEnabled = false 132 | } 133 | } 134 | compileOptions { 135 | sourceCompatibility = JavaVersion.VERSION_1_8 136 | targetCompatibility = JavaVersion.VERSION_1_8 137 | } 138 | dependencies { 139 | debugImplementation(libs.compose.ui.tooling) 140 | } 141 | } 142 | 143 | compose.desktop { 144 | application { 145 | mainClass = "MainKt" 146 | 147 | nativeDistributions { 148 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 149 | packageName = "dev.johnoreilly.gemini" 150 | packageVersion = "1.0.0" 151 | } 152 | } 153 | } 154 | 155 | buildkonfig { 156 | packageName = "dev.johnoreilly.gemini" 157 | 158 | val localPropsFile = rootProject.file("local.properties") 159 | val localProperties = Properties() 160 | if (localPropsFile.exists()) { 161 | runCatching { 162 | localProperties.load(localPropsFile.inputStream()) 163 | }.getOrElse { 164 | it.printStackTrace() 165 | } 166 | } 167 | defaultConfigs { 168 | buildConfigField( 169 | FieldSpec.Type.STRING, 170 | "GEMINI_API_KEY", 171 | localProperties["gemini_api_key"]?.toString() ?: "" 172 | ) 173 | } 174 | 175 | } 176 | 177 | compose.experimental { 178 | web.application {} 179 | } 180 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/actual.kt: -------------------------------------------------------------------------------- 1 | import android.content.Context 2 | import android.graphics.BitmapFactory 3 | import android.os.Build 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import androidx.compose.ui.graphics.asImageBitmap 6 | import com.russhwolf.settings.ObservableSettings 7 | import com.russhwolf.settings.Settings 8 | import com.russhwolf.settings.SharedPreferencesSettings 9 | import dev.johnoreilly.gemini.AndroidJsonDatabase 10 | import dev.johnoreilly.gemini.AndroidTextToSpeech 11 | import dev.johnoreilly.gemini.MainActivity 12 | 13 | actual fun getPlatform(): Platform { 14 | return Platform.Android("Android ${Build.VERSION.SDK_INT}") 15 | } 16 | 17 | actual fun getDataSettings(): Settings { 18 | val sharedPreferences = 19 | MainActivity.instance.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) 20 | return SharedPreferencesSettings(sharedPreferences) 21 | } 22 | 23 | actual fun getDataSettingsFlow(): ObservableSettings? { 24 | val sharedPreferences = 25 | MainActivity.instance.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) 26 | return SharedPreferencesSettings(sharedPreferences) 27 | } 28 | 29 | 30 | actual fun showAlert(message: String) { 31 | android.widget.Toast.makeText( 32 | MainActivity.instance, 33 | message, 34 | android.widget.Toast.LENGTH_SHORT 35 | ).show() 36 | } 37 | 38 | 39 | actual fun getJsonDatabase(): JsonDatabase = AndroidJsonDatabase() 40 | 41 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap { 42 | val bitmap = BitmapFactory.decodeByteArray(this, 0, this.size) 43 | return bitmap.asImageBitmap() 44 | } 45 | 46 | actual fun getTextToSpeech(): TextToSpeech = AndroidTextToSpeech() 47 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/dev/johnoreilly/gemini/AndroidJsonDatabase.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini 2 | 3 | import JsonDatabase 4 | import ListString 5 | import android.content.Context 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flow 8 | import kotlinx.serialization.json.Json 9 | import java.io.File 10 | 11 | class AndroidJsonDatabase : JsonDatabase { 12 | override fun createData(tableName: String, data: ListString): Boolean { 13 | return try { 14 | val jsonString = Json.encodeToString(data) 15 | MainActivity.instance.openFileOutput(tableName, Context.MODE_PRIVATE).use { 16 | it.write(jsonString.toByteArray()) 17 | } 18 | true 19 | } catch (e: Exception) { 20 | e.printStackTrace() 21 | false 22 | } 23 | } 24 | 25 | 26 | override fun getData(tableName: String): ListString { 27 | return try { 28 | val file = File(MainActivity.instance.filesDir, tableName) 29 | if (!file.exists()) return "[]" 30 | Json.decodeFromString(file.readText()) 31 | } catch (e: Exception) { 32 | e.printStackTrace() 33 | "[]" 34 | } 35 | } 36 | 37 | override fun getDataFlow(tableName: String): Flow { 38 | return flow { 39 | val json = getData(tableName) 40 | emit(json) 41 | } 42 | } 43 | 44 | override fun deleteData(tableName: String): Boolean { 45 | return try { 46 | val file = File(MainActivity.instance.filesDir, tableName) 47 | file.delete() 48 | } catch (e: Exception) { 49 | e.printStackTrace() 50 | false 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/dev/johnoreilly/gemini/AndroidTextToSpeech.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini 2 | 3 | import TextToSpeech 4 | import java.util.Locale 5 | 6 | class AndroidTextToSpeech : TextToSpeech { 7 | private val tts = android.speech.tts.TextToSpeech(MainActivity.instance, null) 8 | init { 9 | tts.language = Locale.US 10 | } 11 | override suspend fun speak(text: String) { 12 | tts.speak(text, android.speech.tts.TextToSpeech.QUEUE_FLUSH, null, null) 13 | } 14 | 15 | override suspend fun stop() { 16 | tts.stop() 17 | } 18 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/dev/johnoreilly/gemini/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini 2 | 3 | import App 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.tooling.preview.Preview 9 | 10 | class MainActivity : ComponentActivity() { 11 | companion object{ 12 | lateinit var instance: MainActivity 13 | } 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | instance = this 18 | setContent { 19 | App() 20 | } 21 | } 22 | 23 | } 24 | 25 | @Preview 26 | @Composable 27 | fun AppAndroidPreview() { 28 | App() 29 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GeminiKMP 3 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/assistant.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/chat.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/copy.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/dust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/dust.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/moon.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/rotate_right.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/sound.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/composeApp/src/commonMain/composeResources/drawable/sun.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import androidx.compose.runtime.collectAsState 3 | import androidx.compose.runtime.getValue 4 | import core.app_db.DataManager 5 | import cafe.adriel.voyager.navigator.Navigator 6 | import org.jetbrains.compose.ui.tooling.preview.Preview 7 | import ui.screens.gemini_ai.GeminiAIScreen 8 | import ui.theme.AppTheme 9 | 10 | @Preview 11 | @Composable 12 | fun App() { 13 | val theme by DataManager.getValueFlow(DataManager.THEME_KEY) 14 | .collectAsState(initial = null) 15 | AppTheme( 16 | mode = when (theme?.lowercase()) { 17 | "dark" -> AppTheme.Dark 18 | "light" -> AppTheme.Light 19 | else -> AppTheme.System 20 | } 21 | ) { 22 | Navigator(GeminiAIScreen) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/GeminiApi.kt: -------------------------------------------------------------------------------- 1 | import core.models.ChatMessage 2 | import dev.johnoreilly.gemini.BuildKonfig 3 | import dev.shreyaspatil.ai.client.generativeai.Chat 4 | import dev.shreyaspatil.ai.client.generativeai.GenerativeModel 5 | import dev.shreyaspatil.ai.client.generativeai.type.Content 6 | import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse 7 | import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage 8 | import dev.shreyaspatil.ai.client.generativeai.type.content 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | 12 | class GeminiApi { 13 | companion object { 14 | const val PROMPT_GENERATE_UI = "Act as an Android app developer. " + 15 | "For the image provided, use Jetpack Compose to build the screen so that " + 16 | "the Compose Preview is as close to this image as possible. Also make sure " + 17 | "to include imports and use Material3. Only give code part without any extra " + 18 | "text or description neither at start or end, your response should contain " + 19 | "only code without any explanation." 20 | } 21 | 22 | 23 | private val apiKey = BuildKonfig.GEMINI_API_KEY 24 | 25 | 26 | private val generativeVisionModel = GenerativeModel( 27 | modelName = "gemini-1.5-flash", 28 | apiKey = apiKey 29 | ) 30 | 31 | private val generativeModel = GenerativeModel( 32 | // modelName = "gemini-pro", 33 | modelName = "gemini-2.0-flash", // use this if you are having issues with gemini-pro 34 | apiKey = apiKey 35 | ) 36 | 37 | fun generateContent(prompt: String): Flow { 38 | return generativeModel.generateContentStream(prompt) 39 | } 40 | 41 | fun generateContent(prompt: String, imageData: ByteArray): Flow { 42 | val content = content { 43 | image(PlatformImage(imageData)) 44 | text(prompt) 45 | } 46 | return generativeVisionModel.generateContentStream(content) 47 | } 48 | 49 | fun generateChat(prompt: List): Chat { 50 | val history = mutableListOf() 51 | prompt.forEach { p -> 52 | if (p.sender.lowercase() == "user") { 53 | history.add(content("user") { text(p.message) }) 54 | } else { 55 | history.add(content("assistant") { text(p.message) }) 56 | } 57 | } 58 | return generativeModel.startChat(history) 59 | } 60 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/core/app_db/ChatDbManager.kt: -------------------------------------------------------------------------------- 1 | package core.app_db 2 | 3 | import JsonDatabase 4 | import core.models.ChatMessage 5 | import getJsonDatabase 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import kotlinx.serialization.json.Json 9 | 10 | object ChatDbManager { 11 | 12 | private val jsonDatabase: JsonDatabase = getJsonDatabase() 13 | private const val CHAT_TABLE = "chat.json" 14 | 15 | 16 | suspend fun insertObjectToStore(chatMessage: ChatMessage) { 17 | withContext(Dispatchers.Main) { 18 | val data = jsonDatabase.getData(CHAT_TABLE) 19 | val jdata = Json.decodeFromString>(data) 20 | val jData = jdata.toMutableList() 21 | jData.add(chatMessage) 22 | jsonDatabase.createData(CHAT_TABLE, Json.encodeToString(jData)) 23 | } 24 | } 25 | 26 | suspend fun getObjectToStores(): List { 27 | return withContext(Dispatchers.Main) { 28 | val data = jsonDatabase.getData(CHAT_TABLE) 29 | if (data.isEmpty()) return@withContext emptyList() 30 | Json.decodeFromString>(data).toMutableList() 31 | } 32 | } 33 | 34 | suspend fun deleteAllObjectFromStore() { 35 | withContext(Dispatchers.Main) { 36 | jsonDatabase.deleteData(CHAT_TABLE) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/core/app_db/DataStoreManager.kt: -------------------------------------------------------------------------------- 1 | package core.app_db 2 | 3 | import com.russhwolf.settings.ExperimentalSettingsApi 4 | import com.russhwolf.settings.coroutines.getStringOrNullFlow 5 | import com.russhwolf.settings.set 6 | import getDataSettings 7 | import getDataSettingsFlow 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flowOf 10 | 11 | 12 | @OptIn(ExperimentalSettingsApi::class) 13 | object DataManager { 14 | private val settings = getDataSettings() 15 | private val settingsFlow = getDataSettingsFlow() 16 | const val THEME_KEY = "theme" 17 | 18 | // Save functions 19 | fun setValue(key: String, value: String) = settings.set(key = key, value = value) 20 | 21 | // Regular get functions 22 | fun getValue(key: String): String = settings.getString(key, "null") 23 | 24 | 25 | fun getValueFlow(key: String): Flow { 26 | return settingsFlow?.getStringOrNullFlow(key = key) // Reactive flow if available 27 | ?: flowOf(getValue(key)) // Emit a single value if no flow is available 28 | } 29 | 30 | fun clear(key: String) { 31 | settings.remove(key) 32 | } 33 | 34 | fun clear() { 35 | settings.clear() 36 | settingsFlow?.clear() 37 | } 38 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/core/models/ChatMessage.kt: -------------------------------------------------------------------------------- 1 | package core.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ChatMessage( 7 | val id: Long? = null, 8 | val message: String, 9 | val sender: String, 10 | val time: String 11 | ) 12 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/expect.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.ImageBitmap 2 | import com.russhwolf.settings.ObservableSettings 3 | import com.russhwolf.settings.Settings 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | 7 | sealed class Platform { 8 | data class Android(val message: String) : Platform() 9 | data class Ios(val message: String) : Platform() 10 | data class Desktop(val message: String) : Platform() 11 | data class Web(val message: String) : Platform() 12 | } 13 | 14 | interface JsonDatabase { 15 | fun createData(tableName: String, data: ListString): Boolean 16 | fun getData(tableName: String): ListString 17 | fun getDataFlow(tableName: String): Flow 18 | fun deleteData(tableName: String): Boolean 19 | } 20 | 21 | interface TextToSpeech{ 22 | suspend fun speak(text: String) 23 | suspend fun stop() 24 | } 25 | 26 | expect fun getPlatform(): Platform 27 | 28 | expect fun getJsonDatabase(): JsonDatabase 29 | 30 | expect fun getTextToSpeech(): TextToSpeech 31 | 32 | expect fun ByteArray.toComposeImageBitmap(): ImageBitmap 33 | 34 | expect fun getDataSettings(): Settings 35 | 36 | expect fun getDataSettingsFlow(): ObservableSettings? 37 | 38 | expect fun showAlert(message: String) 39 | 40 | typealias ListString = String 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/Assistant.kt: -------------------------------------------------------------------------------- 1 | package ui.screens.gemini_ai 2 | 3 | import GeminiApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 8 | import androidx.compose.foundation.layout.FlowRow 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.defaultMinSize 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.rememberScrollState 16 | import androidx.compose.foundation.text.selection.SelectionContainer 17 | import androidx.compose.foundation.verticalScroll 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.Clear 20 | import androidx.compose.material3.ButtonDefaults 21 | import androidx.compose.material3.CircularProgressIndicator 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.IconButton 24 | import androidx.compose.material3.MaterialTheme 25 | import androidx.compose.material3.OutlinedButton 26 | import androidx.compose.material3.OutlinedTextField 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.derivedStateOf 30 | import androidx.compose.runtime.getValue 31 | import androidx.compose.runtime.mutableStateOf 32 | import androidx.compose.runtime.remember 33 | import androidx.compose.runtime.rememberCoroutineScope 34 | import androidx.compose.runtime.setValue 35 | import androidx.compose.ui.Alignment 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 38 | import androidx.compose.ui.text.font.FontWeight 39 | import androidx.compose.ui.unit.dp 40 | import com.mikepenz.markdown.m3.Markdown 41 | import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse 42 | import io.github.vinceglb.filekit.PlatformFile 43 | import io.github.vinceglb.filekit.coil.AsyncImage 44 | import io.github.vinceglb.filekit.dialogs.FileKitType 45 | import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher 46 | import io.github.vinceglb.filekit.readBytes 47 | import kotlinx.coroutines.flow.Flow 48 | import kotlinx.coroutines.flow.onCompletion 49 | import kotlinx.coroutines.flow.onStart 50 | import kotlinx.coroutines.launch 51 | 52 | @OptIn(ExperimentalLayoutApi::class) 53 | @Composable 54 | fun AssistantScreen() { 55 | val api = remember { GeminiApi() } 56 | val coroutineScope = rememberCoroutineScope() 57 | var prompt by remember { mutableStateOf("") } 58 | var content by remember { mutableStateOf("") } 59 | var showProgress by remember { mutableStateOf(false) } 60 | var selectedImage by remember { mutableStateOf(null) } 61 | val keyboardController = LocalSoftwareKeyboardController.current 62 | val canClearPrompt by remember { 63 | derivedStateOf { 64 | prompt.isNotBlank() 65 | } 66 | } 67 | 68 | val imagePickerLauncher = rememberFilePickerLauncher(FileKitType.Image) { image -> 69 | coroutineScope.launch { 70 | selectedImage = image 71 | } 72 | } 73 | 74 | Column( 75 | modifier = Modifier 76 | .verticalScroll(rememberScrollState()) 77 | .fillMaxWidth() 78 | ) { 79 | FlowRow( 80 | modifier = Modifier 81 | .fillMaxWidth() 82 | .padding(vertical = 15.dp, horizontal = 15.dp) 83 | ) { 84 | OutlinedTextField( 85 | value = prompt, 86 | onValueChange = { prompt = it }, 87 | modifier = Modifier 88 | .fillMaxSize() 89 | .defaultMinSize(minHeight = 52.dp), 90 | label = { 91 | Text("Search") 92 | }, 93 | trailingIcon = { 94 | if (canClearPrompt) { 95 | IconButton( 96 | onClick = { prompt = "" } 97 | ) { 98 | Icon( 99 | imageVector = Icons.Default.Clear, 100 | contentDescription = "Clear" 101 | ) 102 | } 103 | } 104 | } 105 | ) 106 | 107 | OutlinedButton( 108 | colors = ButtonDefaults.outlinedButtonColors( 109 | containerColor = MaterialTheme.colorScheme.secondary, 110 | contentColor = MaterialTheme.colorScheme.onSecondary 111 | ), 112 | onClick = { 113 | if (prompt.isNotBlank()) { 114 | keyboardController?.hide() 115 | coroutineScope.launch { 116 | println("prompt = $prompt") 117 | content = "" 118 | generateContentAsFlow(api, prompt, selectedImage?.readBytes()) 119 | .onStart { showProgress = true } 120 | .onCompletion { showProgress = false } 121 | .collect { 122 | println("response = ${it.text}") 123 | content += it.text 124 | } 125 | } 126 | } 127 | }, 128 | enabled = prompt.isNotBlank(), 129 | modifier = Modifier 130 | .padding(all = 4.dp) 131 | .weight(1f) 132 | .align(Alignment.CenterVertically) 133 | ) { 134 | Text( 135 | "Submit", 136 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold) 137 | ) 138 | } 139 | 140 | OutlinedButton( 141 | colors = ButtonDefaults.outlinedButtonColors( 142 | containerColor = MaterialTheme.colorScheme.secondary, 143 | contentColor = MaterialTheme.colorScheme.onSecondary 144 | ), 145 | onClick = { imagePickerLauncher.launch() }, 146 | modifier = Modifier 147 | .padding(all = 4.dp) 148 | .weight(1f) 149 | .align(Alignment.CenterVertically) 150 | ) { 151 | Text( 152 | "Select Image", 153 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold) 154 | ) 155 | } 156 | 157 | OutlinedButton( 158 | colors = ButtonDefaults.outlinedButtonColors( 159 | containerColor = MaterialTheme.colorScheme.secondary, 160 | contentColor = MaterialTheme.colorScheme.onSecondary 161 | ), 162 | onClick = { 163 | prompt = GeminiApi.PROMPT_GENERATE_UI 164 | coroutineScope.launch { 165 | content = "" 166 | generateContentAsFlow(api, prompt, selectedImage?.readBytes()) 167 | .onStart { showProgress = true } 168 | .onCompletion { showProgress = false } 169 | .collect { 170 | println("response = ${it.text}") 171 | content += it.text 172 | } 173 | } 174 | 175 | }, 176 | enabled = selectedImage != null, 177 | modifier = Modifier 178 | .fillMaxWidth() 179 | .padding(all = 4.dp) 180 | .align(Alignment.CenterVertically) 181 | ) { 182 | Text( 183 | "Generate Compose UI Code", 184 | style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold) 185 | ) 186 | } 187 | } 188 | 189 | Spacer(Modifier.height(16.dp)) 190 | 191 | selectedImage?.let { 192 | Column( 193 | verticalArrangement = Arrangement.Center, 194 | horizontalAlignment = Alignment.CenterHorizontally 195 | ) { 196 | AsyncImage( 197 | file = selectedImage, 198 | contentDescription = "search_image", 199 | modifier = Modifier.fillMaxSize() 200 | ) 201 | } 202 | } 203 | 204 | Spacer(Modifier.height(16.dp)) 205 | if (showProgress) { 206 | Column( 207 | modifier = Modifier.fillMaxSize(), 208 | verticalArrangement = Arrangement.Center, 209 | horizontalAlignment = Alignment.CenterHorizontally 210 | ) { 211 | CircularProgressIndicator() 212 | } 213 | } else { 214 | SelectionContainer { 215 | Markdown( 216 | modifier = Modifier.fillMaxSize().padding(10.dp), 217 | content = content 218 | ) 219 | } 220 | } 221 | } 222 | } 223 | 224 | private fun generateContentAsFlow( 225 | api: GeminiApi, 226 | prompt: String, 227 | imageData: ByteArray? = null 228 | ): Flow = imageData?.let { imageByteArray -> 229 | api.generateContent(prompt, imageByteArray) 230 | } ?: run { 231 | api.generateContent(prompt) 232 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/Chat.kt: -------------------------------------------------------------------------------- 1 | package ui.screens.gemini_ai 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.gestures.Orientation 5 | import androidx.compose.foundation.gestures.draggable 6 | import androidx.compose.foundation.gestures.rememberDraggableState 7 | import androidx.compose.foundation.gestures.scrollBy 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.items 15 | import androidx.compose.foundation.lazy.rememberLazyListState 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.automirrored.filled.Send 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.IconButton 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.OutlinedTextField 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.rememberCoroutineScope 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.platform.LocalClipboardManager 27 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 28 | import androidx.compose.ui.text.buildAnnotatedString 29 | import androidx.compose.ui.unit.dp 30 | import geminikmp.composeapp.generated.resources.Res 31 | import geminikmp.composeapp.generated.resources.rotate_right 32 | import kotlinx.coroutines.Dispatchers 33 | import kotlinx.coroutines.launch 34 | import org.jetbrains.compose.resources.painterResource 35 | import showAlert 36 | import ui.ChatBubble 37 | import ui.RotatingIcon 38 | import utils.ConnectionState 39 | 40 | 41 | @Composable 42 | fun ChatScreen(viewModel: AiScreenModel) { 43 | val scope = rememberCoroutineScope() 44 | val scrollState = rememberLazyListState() 45 | 46 | Column( 47 | modifier = Modifier.fillMaxSize(), 48 | horizontalAlignment = Alignment.CenterHorizontally 49 | ) { 50 | val keyboardController = LocalSoftwareKeyboardController.current 51 | LazyColumn( 52 | state = scrollState, 53 | modifier = Modifier 54 | .weight(1f) 55 | .draggable( 56 | orientation = Orientation.Vertical, 57 | state = rememberDraggableState { delta -> 58 | scope.launch { 59 | scrollState.scrollBy(-delta) 60 | } 61 | }, 62 | ), 63 | verticalArrangement = Arrangement.Bottom, 64 | horizontalAlignment = Alignment.CenterHorizontally 65 | ) { 66 | items(viewModel.items) { message -> 67 | val clipboardManager = LocalClipboardManager.current 68 | ChatBubble(Modifier.fillMaxWidth(), chatMessage = message) { 69 | if (it.first.trim().lowercase() == "copy") { 70 | clipboardManager.setText( 71 | annotatedString = buildAnnotatedString { 72 | append(text = it.second) 73 | } 74 | ) 75 | showAlert("Copied to clipboard") 76 | } else if (it.first.trim().lowercase() == "speak") { 77 | scope.launch(Dispatchers.Default) { 78 | viewModel.textToSpeech.speak(it.second.replace("*", "")) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | OutlinedTextField( 85 | modifier = Modifier 86 | .fillMaxWidth() 87 | .padding(10.dp), 88 | value = viewModel.prompt, 89 | onValueChange = { viewModel.prompt = it }, 90 | trailingIcon = { 91 | IconButton(onClick = { 92 | keyboardController?.hide() 93 | viewModel.sendMessage() 94 | }) { 95 | if (viewModel.isLoading is ConnectionState.Loading) { 96 | RotatingIcon(painterResource(Res.drawable.rotate_right)) 97 | } else { 98 | Icon( 99 | Icons.AutoMirrored.Filled.Send, 100 | "Send message" 101 | ) 102 | } 103 | } 104 | } 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/GeminiAIScreen.kt: -------------------------------------------------------------------------------- 1 | package ui.screens.gemini_ai 2 | 3 | import Platform 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxHeight 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.MoreVert 15 | import androidx.compose.material3.DropdownMenu 16 | import androidx.compose.material3.DropdownMenuItem 17 | import androidx.compose.material3.ExperimentalMaterial3Api 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.material3.IconButton 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.MenuDefaults 22 | import androidx.compose.material3.Scaffold 23 | import androidx.compose.material3.Text 24 | import androidx.compose.material3.TopAppBar 25 | import androidx.compose.material3.TopAppBarDefaults 26 | import androidx.compose.material3.VerticalDivider 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.collectAsState 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.setValue 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.text.font.FontWeight 34 | import androidx.compose.ui.unit.dp 35 | import cafe.adriel.voyager.core.model.rememberScreenModel 36 | import cafe.adriel.voyager.core.screen.Screen 37 | import geminikmp.composeapp.generated.resources.Res 38 | import geminikmp.composeapp.generated.resources.assistant 39 | import geminikmp.composeapp.generated.resources.chat 40 | import geminikmp.composeapp.generated.resources.dust 41 | import geminikmp.composeapp.generated.resources.moon 42 | import geminikmp.composeapp.generated.resources.sun 43 | import getPlatform 44 | import org.jetbrains.compose.resources.painterResource 45 | 46 | object GeminiAIScreen : Screen { 47 | @OptIn(ExperimentalMaterial3Api::class) 48 | @Composable 49 | override fun Content() { 50 | val viewModel = rememberScreenModel { AiScreenModel() } 51 | var expand by mutableStateOf(false) 52 | val theme by viewModel.theme.collectAsState() // For StateFlow 53 | 54 | 55 | Scaffold( 56 | topBar = { 57 | TopAppBar( 58 | colors = TopAppBarDefaults.topAppBarColors( 59 | containerColor = MaterialTheme.colorScheme.primary, 60 | titleContentColor = MaterialTheme.colorScheme.onPrimary, 61 | actionIconContentColor = MaterialTheme.colorScheme.onPrimary 62 | ), 63 | title = { 64 | Text( 65 | "Gemini", 66 | style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold) 67 | ) 68 | }, 69 | actions = { 70 | IconButton(onClick = { viewModel.updateTheme() }) { 71 | Icon( 72 | modifier = Modifier.size(25.dp), 73 | painter = if (theme?.lowercase() == "dark") 74 | painterResource(Res.drawable.moon) 75 | else 76 | painterResource(Res.drawable.sun), 77 | contentDescription = null 78 | ) 79 | } 80 | if (viewModel.screen == AiScreenType.Chat) { 81 | IconButton(onClick = { viewModel.clearDatabase() }) { 82 | Icon( 83 | modifier = Modifier.size(25.dp), 84 | painter = painterResource(Res.drawable.dust), 85 | contentDescription = null 86 | ) 87 | } 88 | } 89 | if (getPlatform() is Platform.Android || getPlatform() is Platform.Ios) { 90 | IconButton(onClick = { expand = !expand }) { 91 | Icon(Icons.Default.MoreVert, contentDescription = null) 92 | } 93 | DropdownMenu( 94 | containerColor = MaterialTheme.colorScheme.primary, 95 | expanded = expand, 96 | onDismissRequest = { expand = !expand }, 97 | content = { 98 | DropdownMenuItem( 99 | colors = MenuDefaults.itemColors( 100 | textColor = MaterialTheme.colorScheme.onPrimary 101 | ), 102 | enabled = viewModel.screen != AiScreenType.Assistant, 103 | text = { 104 | Text( 105 | "Assistant", 106 | style = MaterialTheme.typography.bodyMedium 107 | ) 108 | }, 109 | onClick = { 110 | viewModel.changeScreen(AiScreenType.Assistant) 111 | expand = !expand 112 | }, 113 | leadingIcon = { 114 | Image( 115 | modifier = Modifier.size(25.dp), 116 | painter = painterResource(Res.drawable.assistant), 117 | contentDescription = "AI Assistant Screen" 118 | ) 119 | } 120 | ) 121 | DropdownMenuItem( 122 | colors = MenuDefaults.itemColors( 123 | textColor = MaterialTheme.colorScheme.onPrimary 124 | ), 125 | enabled = viewModel.screen != AiScreenType.Chat, 126 | text = { 127 | Text( 128 | "Chat", 129 | style = MaterialTheme.typography.bodyMedium 130 | ) 131 | }, 132 | onClick = { 133 | viewModel.changeScreen(AiScreenType.Chat) 134 | expand = !expand 135 | }, 136 | leadingIcon = { 137 | Image( 138 | modifier = Modifier.size(25.dp), 139 | painter = painterResource(Res.drawable.chat), 140 | contentDescription = "AI Chat screen" 141 | ) 142 | } 143 | ) 144 | } 145 | ) 146 | } 147 | } 148 | ) 149 | } 150 | ) { pv -> 151 | Row(Modifier.padding(pv).fillMaxSize()) { 152 | if (getPlatform() is Platform.Desktop || getPlatform() is Platform.Web) { 153 | Column( 154 | Modifier.fillMaxWidth(0.3f).fillMaxHeight(), 155 | verticalArrangement = Arrangement.spacedBy(16.dp) 156 | ) { 157 | DropdownMenuItem( 158 | enabled = viewModel.screen != AiScreenType.Assistant, 159 | text = { 160 | Text( 161 | "Assistant", 162 | style = MaterialTheme.typography.bodyMedium 163 | ) 164 | }, 165 | onClick = { viewModel.changeScreen(AiScreenType.Assistant) }, 166 | leadingIcon = { 167 | Image( 168 | modifier = Modifier.size(50.dp), 169 | painter = painterResource(Res.drawable.assistant), 170 | contentDescription = "AI Assistant Screen" 171 | ) 172 | } 173 | ) 174 | DropdownMenuItem( 175 | enabled = viewModel.screen != AiScreenType.Chat, 176 | text = { Text("Chat", style = MaterialTheme.typography.bodyMedium) }, 177 | onClick = { viewModel.changeScreen(AiScreenType.Chat) }, 178 | leadingIcon = { 179 | Image( 180 | modifier = Modifier.size(50.dp), 181 | painter = painterResource(Res.drawable.chat), 182 | contentDescription = "Chat Assistant Screen" 183 | ) 184 | } 185 | ) 186 | } 187 | VerticalDivider() 188 | } 189 | when (viewModel.screen) { 190 | AiScreenType.Assistant -> AssistantScreen() 191 | AiScreenType.Chat -> ChatScreen(viewModel) 192 | } 193 | } 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/screens/gemini_ai/GeminiAiScreenModel.kt: -------------------------------------------------------------------------------- 1 | package ui.screens.gemini_ai 2 | 3 | import GeminiApi 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import cafe.adriel.voyager.core.model.ScreenModel 8 | import cafe.adriel.voyager.core.model.screenModelScope 9 | import core.app_db.ChatDbManager 10 | import core.app_db.DataManager 11 | import core.models.ChatMessage 12 | import dev.shreyaspatil.ai.client.generativeai.Chat 13 | import dev.shreyaspatil.ai.client.generativeai.type.content 14 | import getTextToSpeech 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.flow.SharingStarted 17 | import kotlinx.coroutines.flow.StateFlow 18 | import kotlinx.coroutines.flow.stateIn 19 | import kotlinx.coroutines.launch 20 | import kotlinx.datetime.Clock 21 | import kotlinx.datetime.TimeZone 22 | import kotlinx.datetime.toLocalDateTime 23 | import utils.ConnectionState 24 | 25 | //data class AiMessage( 26 | // val id: Long? = null, 27 | // val aiModel: String, 28 | // val message: String, 29 | // val time: String = "" 30 | //) 31 | 32 | enum class AiScreenType { 33 | Assistant, 34 | Chat 35 | } 36 | 37 | 38 | class AiScreenModel : ScreenModel { 39 | var prompt by mutableStateOf("") 40 | var screen by mutableStateOf(AiScreenType.Assistant) 41 | var isLoading by mutableStateOf(ConnectionState.Default) 42 | val textToSpeech = getTextToSpeech() 43 | 44 | private val geminiApi = GeminiApi() 45 | 46 | // Expose theme as StateFlow for Compose compatibility 47 | val theme: StateFlow = DataManager.getValueFlow(DataManager.THEME_KEY) 48 | .stateIn( 49 | scope = screenModelScope, 50 | started = SharingStarted.WhileSubscribed(5000L), 51 | initialValue = null // Provide a default value or fallback 52 | ) 53 | var items by mutableStateOf(emptyList()) 54 | private set 55 | private var userChat: Chat by mutableStateOf(geminiApi.generateChat(items)) 56 | 57 | init { 58 | screenModelScope.launch(Dispatchers.Main) { 59 | items = ChatDbManager.getObjectToStores() 60 | userChat = geminiApi.generateChat(items) 61 | } 62 | } 63 | 64 | fun sendMessage() { 65 | screenModelScope.launch(Dispatchers.Main) { 66 | isLoading = ConnectionState.Loading 67 | try { 68 | var nowTime = Clock.System.now().toLocalDateTime(TimeZone.UTC) 69 | items = items + ChatMessage( 70 | sender = "user", 71 | message = prompt, 72 | time = "${nowTime.date} // ${nowTime.hour}:${nowTime.minute}:${nowTime.second}" 73 | ) 74 | ChatDbManager.insertObjectToStore(items.last()) 75 | val result = userChat.sendMessage(content("user") { text(prompt) }) 76 | nowTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) 77 | items = items + ChatMessage( 78 | sender = "model", 79 | message = result.text ?: "Sorry! could not get a response", 80 | time = "${nowTime.date} // ${nowTime.hour}:${nowTime.minute}:${nowTime.second}" 81 | ) 82 | ChatDbManager.insertObjectToStore(items.last()) 83 | prompt = "" 84 | isLoading = ConnectionState.Success("Success") 85 | } catch (e: Exception) { 86 | val nowTime = Clock.System.now().toLocalDateTime(TimeZone.UTC) 87 | isLoading = ConnectionState.Error("Could not generate a response") 88 | items = items + ChatMessage( 89 | sender = "model", 90 | message = "Sorry! could not get a response $e", 91 | time = "${nowTime.date} // ${nowTime.hour}:${nowTime.minute}:${nowTime.second}" 92 | ) 93 | ChatDbManager.insertObjectToStore(items.last()) 94 | println("Error: $e") 95 | prompt = "" 96 | } 97 | } 98 | } 99 | 100 | 101 | // Function to update theme 102 | fun updateTheme() { 103 | screenModelScope.launch(Dispatchers.Main) { 104 | val newTheme = if (theme.value?.lowercase() == "dark") "light" else "dark" 105 | DataManager.setValue(DataManager.THEME_KEY, newTheme) 106 | } 107 | } 108 | 109 | fun clearDatabase() { 110 | screenModelScope.launch(Dispatchers.Main) { 111 | ChatDbManager.deleteAllObjectFromStore() 112 | items = emptyList() 113 | } 114 | } 115 | 116 | fun changeScreen(screen: AiScreenType) { 117 | this.screen = screen 118 | } 119 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/theme/app_theme.kt: -------------------------------------------------------------------------------- 1 | package ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.Color 10 | 11 | enum class AppTheme { 12 | Light, 13 | Dark, 14 | System 15 | } 16 | 17 | private val lightScheme = lightColorScheme( 18 | primary = Color(10, 99, 0), 19 | onPrimary = Color.White, 20 | secondary = Color(99, 40, 0), // Brown 21 | onSecondary = Color(255, 255, 255), // White (RGB: 255, 255, 255) 22 | background = Color.White, 23 | onBackground = Color.Black, 24 | surface = Color.LightGray, 25 | onSurface = Color(0, 0, 0), // Black (RGB: 0, 0, 0) 26 | error = Color.Red, 27 | onError = Color.White 28 | ) 29 | 30 | private val darkScheme = darkColorScheme( 31 | primary = Color(99, 40, 0), // Brown 32 | onPrimary = Color(255, 255, 255), // White (RGB: 255, 255, 255) 33 | secondary = Color(10, 99, 0), // Green 34 | onSecondary = Color.White, 35 | background = Color(0, 0, 0), // Black (RGB: 0, 0, 0) 36 | onBackground = Color(255, 255, 255), // White (RGB: 255, 255, 255) 37 | surface = Color(26, 26, 26), // Dark Gray (RGB: 26, 26, 26) 38 | onSurface = Color(255, 255, 255), // White (RGB: 255, 255, 255) 39 | error = Color(255, 176, 32), // Red (RGB: 255, 176, 32) 40 | onError = Color(0, 0, 0) // Black (RGB: 0, 0, 0) 41 | ) 42 | 43 | 44 | 45 | @Composable 46 | fun AppTheme( 47 | mode: AppTheme = AppTheme.System, 48 | content: @Composable () -> Unit, 49 | ) { 50 | val isDark = isSystemInDarkTheme() 51 | val colorScheme = remember(mode) { 52 | when (mode) { 53 | AppTheme.Light -> lightScheme 54 | AppTheme.Dark -> darkScheme 55 | AppTheme.System -> if (isDark) darkScheme else lightScheme 56 | } 57 | } 58 | MaterialTheme( 59 | colorScheme = colorScheme, 60 | typography = MaterialTheme.typography, 61 | content = content 62 | ) 63 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widgets.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.Image 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.Arrangement 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.Row 14 | import androidx.compose.foundation.layout.Spacer 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.width 19 | import androidx.compose.foundation.layout.wrapContentSize 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.IconButton 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.graphicsLayer 30 | import androidx.compose.ui.graphics.painter.Painter 31 | import androidx.compose.ui.text.font.FontWeight 32 | import androidx.compose.ui.unit.Dp 33 | import androidx.compose.ui.unit.dp 34 | import com.mikepenz.markdown.m3.Markdown 35 | import core.models.ChatMessage 36 | import geminikmp.composeapp.generated.resources.Res 37 | import geminikmp.composeapp.generated.resources.copy 38 | import geminikmp.composeapp.generated.resources.sound 39 | import org.jetbrains.compose.resources.painterResource 40 | 41 | 42 | @Composable 43 | fun TextIcon( 44 | modifier: Modifier = Modifier, 45 | leadingIcon: @Composable (() -> Unit)? = null, 46 | text: @Composable () -> Unit, 47 | trailingIcon: @Composable (() -> Unit)? = null, 48 | vAlignment: Alignment.Vertical = Alignment.Top, 49 | hArrangement: Arrangement.Horizontal 50 | ) { 51 | Row(modifier, horizontalArrangement = hArrangement, verticalAlignment = vAlignment) { 52 | if (leadingIcon != null) { 53 | leadingIcon() 54 | Spacer(Modifier.width(8.dp)) 55 | } 56 | text() 57 | if (trailingIcon != null) { 58 | Spacer(Modifier.width(8.dp)) 59 | trailingIcon() 60 | } 61 | } 62 | } 63 | 64 | 65 | @Composable 66 | fun RotatingIcon( 67 | painterIcon: Painter, 68 | modifier: Modifier = Modifier, 69 | sizeDpSize: Dp = 64.dp, 70 | durationMillis: Int = 2000 // Duration of one full rotation 71 | ) { 72 | val infiniteTransition = rememberInfiniteTransition(label = "Rotation") 73 | 74 | val rotationAngle by infiniteTransition.animateFloat( 75 | initialValue = 0f, 76 | targetValue = 360f, 77 | animationSpec = infiniteRepeatable( 78 | animation = tween( 79 | durationMillis = durationMillis, 80 | easing = LinearEasing 81 | ), // Smooth linear rotation 82 | repeatMode = RepeatMode.Restart 83 | ), 84 | label = "Rotation Animation" 85 | ) 86 | 87 | Icon( 88 | painter = painterIcon, 89 | contentDescription = "Rotating Icon", 90 | modifier = modifier 91 | .graphicsLayer(rotationZ = rotationAngle) // Apply rotation 92 | .size(sizeDpSize) // Icon size 93 | ) 94 | } 95 | 96 | 97 | @Composable 98 | fun ChatBubble( 99 | modifier: Modifier = Modifier, 100 | chatMessage: ChatMessage, 101 | onClick: ((Pair) -> Unit)? = null 102 | ) { 103 | val hArrange = 104 | if (chatMessage.sender.lowercase() == "user") Arrangement.End else Arrangement.Start 105 | val vAlign = if (chatMessage.sender.lowercase() == "user") Alignment.End else Alignment.Start 106 | Row( 107 | modifier = modifier, 108 | horizontalArrangement = hArrange 109 | ) { 110 | Column( 111 | Modifier.fillMaxWidth(0.9f), 112 | horizontalAlignment = vAlign 113 | ) { 114 | Markdown( 115 | modifier = Modifier 116 | .wrapContentSize() 117 | .padding(4.dp) 118 | .background(MaterialTheme.colorScheme.background, MaterialTheme.shapes.small) 119 | .padding(8.dp), 120 | content = chatMessage.message 121 | ) 122 | if (onClick != null) { 123 | Row (verticalAlignment = Alignment.CenterVertically){ 124 | // speak button icon // 125 | IconButton( 126 | onClick = { onClick(Pair("speak", chatMessage.message)) } 127 | ) { 128 | Image( 129 | painter = painterResource(Res.drawable.sound), 130 | contentDescription = "Speak", 131 | modifier = Modifier.size(24.dp) 132 | ) 133 | } 134 | // copy button icon // 135 | IconButton( 136 | onClick = { onClick(Pair("copy", chatMessage.message)) } 137 | ) { 138 | Image( 139 | painter = painterResource(Res.drawable.copy), 140 | contentDescription = "Copy", 141 | modifier = Modifier.size(24.dp) 142 | ) 143 | } 144 | Column { 145 | val time = remember { chatMessage.time.split("//") } 146 | Text( 147 | text = time[0], 148 | style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.W300) 149 | ) 150 | Text( 151 | text = time[1], 152 | style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.W300) 153 | ) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/utils/AppConstants.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | object AppConstants { 4 | 5 | const val TEST_DATA1 = """ 6 | ![kotlin-version](https://img.shields.io/badge/kotlin-2.0.0-blue?logo=kotlin) 7 | 8 | Kotlin/Compose Multiplatform sample to demonstrate Gemini Generative AI APIs (text and image based queries). 9 | Uses [Generative AI SDK](https://github.com/PatilShreyas/generative-ai-kmp). 10 | 11 | 12 | Running on 13 | * iOS 14 | * Android 15 | * Wear OS (contributed by https://github.com/yschimke) 16 | * Desktop 17 | * Web (Wasm) 18 | 19 | Set your Gemini API key (`gemini_api_key`) in `local.properties` 20 | 21 | Related posts: 22 | * [Exploring use of Gemini Generative AI APIs in a Kotlin/Compose Multiplatform project](https://johnoreilly.dev/posts/gemini-kotlin-multiplatform/) 23 | 24 | 25 | 26 | ## Screenshots 27 | 28 | ### iOS 29 | 30 | ![Simulator Screenshot - iPhone 15 Pro - 2024-01-19 at 19 15 53](https://github.com/joreilly/GeminiKMP/assets/6302/91e5d4f5-7cb5-40d4-95fb-c5d87bac7918) 31 | 32 | 33 | ### Android 34 | 35 | 36 | ![Screenshot_1705691519](https://github.com/joreilly/GeminiKMP/assets/6302/668145c1-1dcf-4cd5-8b1d-a04f7ebd6866) 37 | 38 | 39 | ### Compose for Desktop 40 | 41 | Screenshot 2024-01-19 at 19 03 52 42 | 43 | 44 | Screenshot 2024-01-14 at 17 41 26 45 | 46 | 47 | 48 | 49 | 50 | ### Wasm based Compose for Web 51 | 52 | Screenshot 2023-12-31 at 13 01 02 53 | 54 | Screenshot 2024-01-14 at 19 26 05 55 | 56 | ## Full set of Kotlin Multiplatform/Compose/SwiftUI samples 57 | 58 | * PeopleInSpace (https://github.com/joreilly/PeopleInSpace) 59 | * GalwayBus (https://github.com/joreilly/GalwayBus) 60 | * Confetti (https://github.com/joreilly/Confetti) 61 | * BikeShare (https://github.com/joreilly/BikeShare) 62 | * FantasyPremierLeague (https://github.com/joreilly/FantasyPremierLeague) 63 | * ClimateTrace (https://github.com/joreilly/ClimateTraceKMP) 64 | * GeminiKMP (https://github.com/joreilly/GeminiKMP) 65 | * MortyComposeKMM (https://github.com/joreilly/MortyComposeKMM) 66 | * StarWars (https://github.com/joreilly/StarWars) 67 | * WordMasterKMP (https://github.com/joreilly/WordMasterKMP) 68 | * Chip-8 (https://github.com/joreilly/chip-8) 69 | """ 70 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/utils/ConnectionState.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | sealed class ConnectionState { 4 | data object Loading : ConnectionState() 5 | data class Error(val errorMessage: String) : ConnectionState() 6 | data class Success(val data: String) : ConnectionState() 7 | data object Default : ConnectionState() 8 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/DeskTopJsonDB.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.flow.Flow 2 | import kotlinx.coroutines.flow.flow 3 | import java.io.File 4 | 5 | private const val INFO = """ 6 | Gemini KMP folder and its contents can be deleted. (*/.geminikmp/GeminiKmp/* 7 | The .json file is been used by the GeminiKMP app to store chat history. If this folder is deleted 8 | and the app is not installed the app will automatically create this folder. 9 | 10 | from co-developer 11 | (Ohior) 12 | (\__/) 13 | (•ㅅ•) *sniff sniff* 14 | /   づ 15 | * * 16 | * 🐭 R A T * 17 | * * 18 | ************* 19 | """ 20 | 21 | class DeskTopJsonDB : JsonDatabase { 22 | private fun getFilePath(tableName: String): File { 23 | val appDir = File(System.getProperty("user.home"), ".geminikmp/GeminiKMP") 24 | .apply { mkdirs() } 25 | val file = File(appDir.path, tableName) 26 | val infoFile = File(appDir.path, "INFO") 27 | if (!infoFile.exists()) infoFile.writeText(INFO) 28 | if (!file.exists()) file.createNewFile() 29 | return file 30 | } 31 | 32 | override fun createData(tableName: String, data: ListString): Boolean { 33 | return try { 34 | getFilePath(tableName).writeText(data) 35 | true 36 | } catch (e: Exception) { 37 | e.printStackTrace() 38 | false 39 | } 40 | 41 | } 42 | 43 | override fun deleteData(tableName: String): Boolean { 44 | return try { 45 | getFilePath(tableName).delete() 46 | } catch (e: Exception) { 47 | e.printStackTrace() 48 | false 49 | } 50 | } 51 | 52 | override fun getData(tableName: String): ListString { 53 | return try { 54 | val file = getFilePath(tableName) 55 | file.readText().ifEmpty { "[]" } 56 | } catch (e: Exception) { 57 | println(e.toString()) 58 | "[]" 59 | } 60 | } 61 | 62 | override fun getDataFlow(tableName: String): Flow { 63 | return flow { 64 | val json = getData(tableName) 65 | emit(json) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/DesktopTextToSpeech.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.CoroutineScope 2 | import kotlinx.coroutines.Dispatchers 3 | import kotlinx.coroutines.launch 4 | import nl.marc_apps.tts.TextToSpeechFactory 5 | import nl.marc_apps.tts.TextToSpeechInstance 6 | 7 | 8 | class DesktopTextToSpeech : TextToSpeech { 9 | private var textToSpeech: TextToSpeechInstance? = null 10 | 11 | init { 12 | CoroutineScope(Dispatchers.Default).launch { 13 | textToSpeech = TextToSpeechFactory().createOrNull() 14 | } 15 | } 16 | 17 | override suspend fun speak(text: String) { 18 | textToSpeech?.say(text) 19 | } 20 | 21 | override suspend fun stop() { 22 | textToSpeech?.stop() 23 | textToSpeech?.close() 24 | } 25 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/actual.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.ImageBitmap 2 | import androidx.compose.ui.graphics.toComposeImageBitmap 3 | import com.russhwolf.settings.ObservableSettings 4 | import com.russhwolf.settings.PreferencesSettings 5 | import com.russhwolf.settings.Settings 6 | import org.jetbrains.skia.Image 7 | import java.util.prefs.Preferences 8 | 9 | actual fun getJsonDatabase(): JsonDatabase = DeskTopJsonDB() 10 | 11 | actual fun showAlert(message: String) { 12 | javax.swing.JOptionPane.showMessageDialog( 13 | null, 14 | message, 15 | "Notification", 16 | javax.swing.JOptionPane.INFORMATION_MESSAGE 17 | ) 18 | } 19 | 20 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap = 21 | Image.makeFromEncoded(this).toComposeImageBitmap() 22 | 23 | actual fun getPlatform(): Platform { 24 | return Platform.Desktop( 25 | System.getProperty("os.name") + " " + System.getProperty("os.version") 26 | ) 27 | } 28 | 29 | actual fun getDataSettings(): Settings { 30 | return PreferencesSettings(Preferences.userRoot().node("app_preferences")) 31 | } 32 | 33 | actual fun getDataSettingsFlow(): ObservableSettings? { 34 | return PreferencesSettings(Preferences.userRoot().node("app_preferences")) 35 | } 36 | 37 | actual fun getTextToSpeech(): TextToSpeech = DesktopTextToSpeech() 38 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.desktop.ui.tooling.preview.Preview 2 | import androidx.compose.runtime.Composable 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | 6 | 7 | fun main() = application { 8 | Window(onCloseRequest = ::exitApplication, title = "GeminiKMP") { 9 | App() 10 | } 11 | } 12 | 13 | @Preview 14 | @Composable 15 | fun AppDesktopPreview() { 16 | App() 17 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/IosJsonDB.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.cinterop.BetaInteropApi 2 | import kotlinx.cinterop.ExperimentalForeignApi 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flow 5 | import kotlinx.serialization.json.Json 6 | import platform.Foundation.NSDocumentDirectory 7 | import platform.Foundation.NSFileManager 8 | import platform.Foundation.NSSearchPathForDirectoriesInDomains 9 | import platform.Foundation.NSString 10 | import platform.Foundation.NSUTF8StringEncoding 11 | import platform.Foundation.NSUserDomainMask 12 | import platform.Foundation.create 13 | import platform.Foundation.dataUsingEncoding 14 | import platform.Foundation.stringWithContentsOfFile 15 | import platform.Foundation.writeToFile 16 | 17 | class IosJsonDB : JsonDatabase { 18 | 19 | 20 | private fun getFilePath(tableName: String): String { 21 | val dir = NSSearchPathForDirectoriesInDomains( 22 | directory = NSDocumentDirectory, 23 | domainMask = NSUserDomainMask, 24 | expandTilde = true 25 | ).first() as String 26 | return "$dir/$tableName" 27 | } 28 | 29 | @OptIn(BetaInteropApi::class) 30 | override fun createData(tableName: String, data: ListString): Boolean { 31 | return try { 32 | val json = Json.encodeToString(data) 33 | val path = getFilePath(tableName) 34 | val nsData = NSString.create(string = json).dataUsingEncoding(NSUTF8StringEncoding) 35 | nsData?.writeToFile(path, atomically = true) ?: false 36 | } catch (e: Exception) { 37 | println("createData error: ${e.message}") 38 | false 39 | } 40 | } 41 | 42 | @OptIn(ExperimentalForeignApi::class) 43 | override fun deleteData(tableName: String): Boolean { 44 | return try { 45 | val path = getFilePath(tableName) 46 | val fileManager = NSFileManager.defaultManager 47 | fileManager.removeItemAtPath(path, null) 48 | true 49 | } catch (e: Exception) { 50 | println("deleteData error: ${e.message}") 51 | false 52 | } 53 | } 54 | 55 | @OptIn(ExperimentalForeignApi::class) 56 | override fun getData(tableName: String): ListString { 57 | return try { 58 | val path = getFilePath(tableName) 59 | val nsString = NSString.stringWithContentsOfFile( 60 | path, 61 | encoding = NSUTF8StringEncoding, 62 | error = null 63 | ) 64 | if (nsString != null) { 65 | Json.decodeFromString(nsString) 66 | }else "[]" 67 | } catch (e: Exception) { 68 | println("readRawJson error: ${e.message}") 69 | "[]" 70 | } 71 | } 72 | 73 | override fun getDataFlow(tableName: String): Flow { 74 | return flow { 75 | val json = getData(tableName) 76 | emit(json) 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/IosTextToSpeech.kt: -------------------------------------------------------------------------------- 1 | import platform.AVFAudio.AVSpeechSynthesisVoice 2 | import platform.AVFAudio.AVSpeechSynthesizer 3 | import platform.AVFAudio.AVSpeechUtterance 4 | 5 | 6 | 7 | 8 | class IosTextToSpeech : TextToSpeech { 9 | private val synthesizer = AVSpeechSynthesizer() 10 | 11 | override suspend fun speak(text: String) { 12 | val utterance = AVSpeechUtterance.speechUtteranceWithString(text) 13 | // utterance.voice = AVSpeechSynthesisVoice().voiceWithLanguage("en-US") 14 | synthesizer.speakUtterance(utterance) 15 | } 16 | 17 | override suspend fun stop() { 18 | synthesizer.stopSpeakingAtBoundary(platform.AVFAudio.AVSpeechBoundary.AVSpeechBoundaryWord) 19 | } 20 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | 3 | fun MainViewController() = ComposeUIViewController { App() } 4 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/actual.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.ImageBitmap 2 | import androidx.compose.ui.graphics.toComposeImageBitmap 3 | import com.russhwolf.settings.NSUserDefaultsSettings 4 | import com.russhwolf.settings.ObservableSettings 5 | import com.russhwolf.settings.Settings 6 | import org.jetbrains.skia.Image 7 | import platform.Foundation.NSUserDefaults 8 | import platform.UIKit.UIAlertController 9 | import platform.UIKit.UIApplication 10 | import platform.UIKit.UIDevice 11 | 12 | actual fun getJsonDatabase(): JsonDatabase = IosJsonDB() 13 | 14 | actual fun showAlert(message: String) { 15 | val alertController = UIAlertController 16 | .alertControllerWithTitle(null, message, 1000L) 17 | val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController 18 | rootViewController?.presentViewController(alertController, true, null) 19 | 20 | } 21 | 22 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap { 23 | return Image.makeFromEncoded(this).toComposeImageBitmap() 24 | } 25 | 26 | actual fun getPlatform(): Platform { 27 | return Platform.Ios( 28 | UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 29 | ) 30 | } 31 | 32 | actual fun getDataSettings(): Settings { 33 | return NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) 34 | } 35 | 36 | actual fun getDataSettingsFlow(): ObservableSettings? { 37 | return NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) 38 | } 39 | 40 | 41 | actual fun getTextToSpeech(): TextToSpeech = IosTextToSpeech() 42 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/WebJsonDB.kt: -------------------------------------------------------------------------------- 1 | 2 | import kotlinx.browser.localStorage 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flow 5 | import kotlinx.serialization.json.Json 6 | 7 | class WebJsonDB : JsonDatabase { 8 | 9 | override fun createData(tableName: String, data: ListString): Boolean { 10 | return try { 11 | val json = Json.encodeToString(data) 12 | localStorage.setItem(tableName, json) 13 | true 14 | } catch (e: Exception) { 15 | println("createData error $e") 16 | false 17 | } 18 | } 19 | 20 | override fun deleteData(tableName: String): Boolean { 21 | return try { 22 | localStorage.removeItem(tableName) 23 | true 24 | } catch (e: Exception) { 25 | println("createData error $e") 26 | false 27 | } 28 | } 29 | 30 | override fun getData(tableName: String): ListString { 31 | return try { 32 | val jsonString = localStorage.getItem(tableName) ?: "[]" 33 | Json.decodeFromString(jsonString) 34 | } catch (e: Exception) { 35 | println("createData error $e") 36 | "[]" 37 | } 38 | } 39 | 40 | override fun getDataFlow(tableName: String): Flow { 41 | return flow { 42 | val json = getData(tableName) 43 | emit(json) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/WebTextToSpeech.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.CoroutineScope 2 | import kotlinx.coroutines.Dispatchers 3 | import kotlinx.coroutines.launch 4 | import nl.marc_apps.tts.TextToSpeechFactory 5 | import nl.marc_apps.tts.TextToSpeechInstance 6 | 7 | 8 | class WebTextToSpeech : TextToSpeech { 9 | private var textToSpeech: TextToSpeechInstance? = null 10 | 11 | init { 12 | CoroutineScope(Dispatchers.Default).launch { 13 | textToSpeech = TextToSpeechFactory().createOrNull() 14 | } 15 | } 16 | 17 | override suspend fun speak(text: String) { 18 | textToSpeech?.say(text) 19 | } 20 | 21 | override suspend fun stop() { 22 | textToSpeech?.stop() 23 | textToSpeech?.close() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/actual.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.ImageBitmap 2 | import androidx.compose.ui.graphics.toComposeImageBitmap 3 | import com.russhwolf.settings.ObservableSettings 4 | import com.russhwolf.settings.Settings 5 | import com.russhwolf.settings.StorageSettings 6 | import kotlinx.browser.window 7 | import org.jetbrains.skia.Image 8 | 9 | 10 | //actual suspend fun createDatabaseDriver(): MySqlDriver { 11 | //// return WebWorkerDriver( 12 | //// Worker( 13 | //// js("""new URL("@cashapp/sqldelight-sqljs-worker/sqljs.worker.js", import.meta.url)""") as String 14 | //// ) 15 | //// ).also { ChatDatabase.Schema.awaitCreate(it) } 16 | // throw NotImplementedError("createDatabaseDriver is not implemented on wasm, because sqldriver exist for only js") 17 | //} 18 | //actual suspend fun chatDatabase(): ChatDatabase? = null 19 | 20 | actual fun getJsonDatabase(): JsonDatabase = WebJsonDB() 21 | 22 | actual fun showAlert(message: String) { 23 | window.alert(message) 24 | } 25 | 26 | 27 | actual fun ByteArray.toComposeImageBitmap(): ImageBitmap { 28 | return Image.makeFromEncoded(this).toComposeImageBitmap() 29 | } 30 | 31 | actual fun getPlatform(): Platform { 32 | return Platform.Web("Web wasm") 33 | } 34 | 35 | 36 | actual fun getDataSettings(): Settings { 37 | return StorageSettings() 38 | } 39 | 40 | actual fun getDataSettingsFlow(): ObservableSettings? { 41 | return null 42 | } 43 | 44 | actual fun getTextToSpeech(): TextToSpeech = WebTextToSpeech() 45 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.CanvasBasedWindow 3 | 4 | @OptIn(ExperimentalComposeUiApi::class) 5 | fun main() { 6 | CanvasBasedWindow(canvasElementId = "ComposeTarget") { 7 | App() 8 | } 9 | } 10 | // For jsMain 11 | //@OptIn(ExperimentalComposeUiApi::class) 12 | //fun main() { 13 | // onWasmReady { 14 | // val body = document.body ?: return@onWasmReady 15 | // CanvasBasedWindow(canvasElementId = "ComposeTarget") { 16 | //// ComposeViewport(body) { 17 | // App() 18 | // } 19 | // } 20 | //} -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compose App 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | #Gradle 4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 5 | 6 | 7 | #Android 8 | android.nonTransitiveRClass=true 9 | android.useAndroidX=true 10 | 11 | #Compose 12 | org.jetbrains.compose.experimental.jscanvas.enabled=true 13 | org.jetbrains.compose.experimental.wasm.enabled=true 14 | 15 | #MPP 16 | kotlin.mpp.androidSourceSetLayoutVersion=2 17 | kotlin.mpp.enableCInteropCommonization=true 18 | 19 | #Development 20 | development=true 21 | kotlin.native.ignoreDisabledTargets=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.20" 3 | kotlinx-coroutines = "1.10.1" 4 | 5 | agp = "8.9.1" 6 | android-compileSdk = "34" 7 | android-minSdk = "24" 8 | android-targetSdk = "34" 9 | androidx-activityCompose = "1.10.1" 10 | compose = "1.7.8" 11 | compose-plugin = "1.7.1" 12 | composeWindowSize = "0.5.0" 13 | filekit = "0.10.0-beta01" 14 | generativeai = "0.9.0-1.1.0" 15 | horologist = "0.6.20" 16 | junit = "4.13.2" 17 | multiplatformSettings = "1.3.0" 18 | buildkonfig = "0.15.2" 19 | markdownRenderer = "0.27.0" 20 | wearCompose = "1.4.1" 21 | voyagerVersion = "1.1.0-beta03" 22 | kotlinx-serialization = "1.8.1" 23 | 24 | 25 | [libraries] 26 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 27 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } 28 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 29 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 30 | 31 | generativeai = { module = "dev.shreyaspatil.generativeai:generativeai-google", version.ref = "generativeai" } 32 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 33 | 34 | androidx-wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "wearCompose" } 35 | 36 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 37 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 38 | compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } 39 | compose-window-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "composeWindowSize" } 40 | 41 | filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" } 42 | filekit-coil = { module = "io.github.vinceglb:filekit-coil", version.ref = "filekit" } 43 | 44 | markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" } 45 | markdown-renderer-core = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" } 46 | 47 | horologist-composables = { module = "com.google.android.horologist:horologist-composables", version.ref = "horologist" } 48 | horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } 49 | horologist-ai-ui = { module = "com.google.android.horologist:horologist-ai-ui", version.ref = "horologist" } 50 | compose-material-iconsext = "androidx.compose.material:material-icons-extended:1.7.8" 51 | 52 | desugar = "com.android.tools:desugar_jdk_libs:2.1.2" 53 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 54 | multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } 55 | multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } 56 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyagerVersion" } 57 | voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyagerVersion" } 58 | 59 | [plugins] 60 | androidApplication = { id = "com.android.application", version.ref = "agp" } 61 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 62 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 63 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 64 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 65 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 66 | buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" } 67 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 04 12:55:16 WAT 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=dev.johnoreilly.gemini.GeminiKMP 3 | APP_NAME=GeminiKMP -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 18 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 19 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 20 | 7555FF7B242A565900829871 /* GeminiKMP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeminiKMP.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXGroup section */ 27 | 058557D7273AAEEB004C7B11 /* Preview Content */ = { 28 | isa = PBXGroup; 29 | children = ( 30 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, 31 | ); 32 | path = "Preview Content"; 33 | sourceTree = ""; 34 | }; 35 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = { 36 | isa = PBXGroup; 37 | children = ( 38 | ); 39 | name = Frameworks; 40 | sourceTree = ""; 41 | }; 42 | 7555FF72242A565900829871 = { 43 | isa = PBXGroup; 44 | children = ( 45 | AB1DB47929225F7C00F7AF9C /* Configuration */, 46 | 7555FF7D242A565900829871 /* iosApp */, 47 | 7555FF7C242A565900829871 /* Products */, 48 | 42799AB246E5F90AF97AA0EF /* Frameworks */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 7555FF7C242A565900829871 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 7555FF7B242A565900829871 /* GeminiKMP.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 7555FF7D242A565900829871 /* iosApp */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 058557BA273AAA24004C7B11 /* Assets.xcassets */, 64 | 7555FF82242A565900829871 /* ContentView.swift */, 65 | 7555FF8C242A565B00829871 /* Info.plist */, 66 | 2152FB032600AC8F00CF470E /* iOSApp.swift */, 67 | 058557D7273AAEEB004C7B11 /* Preview Content */, 68 | ); 69 | path = iosApp; 70 | sourceTree = ""; 71 | }; 72 | AB1DB47929225F7C00F7AF9C /* Configuration */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | AB3632DC29227652001CCB65 /* Config.xcconfig */, 76 | ); 77 | path = Configuration; 78 | sourceTree = ""; 79 | }; 80 | /* End PBXGroup section */ 81 | 82 | /* Begin PBXNativeTarget section */ 83 | 7555FF7A242A565900829871 /* iosApp */ = { 84 | isa = PBXNativeTarget; 85 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; 86 | buildPhases = ( 87 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, 88 | 7555FF77242A565900829871 /* Sources */, 89 | 7555FF79242A565900829871 /* Resources */, 90 | ); 91 | buildRules = ( 92 | ); 93 | dependencies = ( 94 | ); 95 | name = iosApp; 96 | productName = iosApp; 97 | productReference = 7555FF7B242A565900829871 /* GeminiKMP.app */; 98 | productType = "com.apple.product-type.application"; 99 | }; 100 | /* End PBXNativeTarget section */ 101 | 102 | /* Begin PBXProject section */ 103 | 7555FF73242A565900829871 /* Project object */ = { 104 | isa = PBXProject; 105 | attributes = { 106 | LastSwiftUpdateCheck = 1130; 107 | LastUpgradeCheck = 1130; 108 | ORGANIZATIONNAME = orgName; 109 | TargetAttributes = { 110 | 7555FF7A242A565900829871 = { 111 | CreatedOnToolsVersion = 11.3.1; 112 | }; 113 | }; 114 | }; 115 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; 116 | compatibilityVersion = "Xcode 9.3"; 117 | developmentRegion = en; 118 | hasScannedForEncodings = 0; 119 | knownRegions = ( 120 | en, 121 | Base, 122 | ); 123 | mainGroup = 7555FF72242A565900829871; 124 | productRefGroup = 7555FF7C242A565900829871 /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 7555FF7A242A565900829871 /* iosApp */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | 7555FF79242A565900829871 /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, 139 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | /* End PBXResourcesBuildPhase section */ 144 | 145 | /* Begin PBXShellScriptBuildPhase section */ 146 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { 147 | isa = PBXShellScriptBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | ); 151 | inputFileListPaths = ( 152 | ); 153 | inputPaths = ( 154 | ); 155 | name = "Compile Kotlin Framework"; 156 | outputFileListPaths = ( 157 | ); 158 | outputPaths = ( 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | shellPath = /bin/sh; 162 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; 163 | }; 164 | /* End PBXShellScriptBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | 7555FF77242A565900829871 /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 172 | 7555FF83242A565900829871 /* ContentView.swift in Sources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXSourcesBuildPhase section */ 177 | 178 | /* Begin XCBuildConfiguration section */ 179 | 7555FFA3242A565B00829871 /* Debug */ = { 180 | isa = XCBuildConfiguration; 181 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 182 | buildSettings = { 183 | ALWAYS_SEARCH_USER_PATHS = NO; 184 | CLANG_ANALYZER_NONNULL = YES; 185 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 187 | CLANG_CXX_LIBRARY = "libc++"; 188 | CLANG_ENABLE_MODULES = YES; 189 | CLANG_ENABLE_OBJC_ARC = YES; 190 | CLANG_ENABLE_OBJC_WEAK = YES; 191 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 192 | CLANG_WARN_BOOL_CONVERSION = YES; 193 | CLANG_WARN_COMMA = YES; 194 | CLANG_WARN_CONSTANT_CONVERSION = YES; 195 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 196 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 197 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 198 | CLANG_WARN_EMPTY_BODY = YES; 199 | CLANG_WARN_ENUM_CONVERSION = YES; 200 | CLANG_WARN_INFINITE_RECURSION = YES; 201 | CLANG_WARN_INT_CONVERSION = YES; 202 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 203 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 204 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 205 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 206 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 207 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 208 | CLANG_WARN_STRICT_PROTOTYPES = YES; 209 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 210 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | COPY_PHASE_STRIP = NO; 214 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | ENABLE_TESTABILITY = YES; 217 | GCC_C_LANGUAGE_STANDARD = gnu11; 218 | GCC_DYNAMIC_NO_PIC = NO; 219 | GCC_NO_COMMON_BLOCKS = YES; 220 | GCC_OPTIMIZATION_LEVEL = 0; 221 | GCC_PREPROCESSOR_DEFINITIONS = ( 222 | "DEBUG=1", 223 | "$(inherited)", 224 | ); 225 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 226 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 227 | GCC_WARN_UNDECLARED_SELECTOR = YES; 228 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 229 | GCC_WARN_UNUSED_FUNCTION = YES; 230 | GCC_WARN_UNUSED_VARIABLE = YES; 231 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 232 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 233 | MTL_FAST_MATH = YES; 234 | ONLY_ACTIVE_ARCH = YES; 235 | SDKROOT = iphoneos; 236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 238 | }; 239 | name = Debug; 240 | }; 241 | 7555FFA4242A565B00829871 /* Release */ = { 242 | isa = XCBuildConfiguration; 243 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_NONNULL = YES; 247 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_ENABLE_OBJC_WEAK = YES; 253 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 254 | CLANG_WARN_BOOL_CONVERSION = YES; 255 | CLANG_WARN_COMMA = YES; 256 | CLANG_WARN_CONSTANT_CONVERSION = YES; 257 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 258 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 259 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 260 | CLANG_WARN_EMPTY_BODY = YES; 261 | CLANG_WARN_ENUM_CONVERSION = YES; 262 | CLANG_WARN_INFINITE_RECURSION = YES; 263 | CLANG_WARN_INT_CONVERSION = YES; 264 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | COPY_PHASE_STRIP = NO; 276 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 277 | ENABLE_NS_ASSERTIONS = NO; 278 | ENABLE_STRICT_OBJC_MSGSEND = YES; 279 | GCC_C_LANGUAGE_STANDARD = gnu11; 280 | GCC_NO_COMMON_BLOCKS = YES; 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 288 | MTL_ENABLE_DEBUG_INFO = NO; 289 | MTL_FAST_MATH = YES; 290 | SDKROOT = iphoneos; 291 | SWIFT_COMPILATION_MODE = wholemodule; 292 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 293 | VALIDATE_PRODUCT = YES; 294 | }; 295 | name = Release; 296 | }; 297 | 7555FFA6242A565B00829871 /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 301 | CODE_SIGN_IDENTITY = "Apple Development"; 302 | CODE_SIGN_STYLE = Manual; 303 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 304 | DEVELOPMENT_TEAM = ""; 305 | ENABLE_PREVIEWS = YES; 306 | FRAMEWORK_SEARCH_PATHS = ( 307 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 308 | ); 309 | INFOPLIST_FILE = iosApp/Info.plist; 310 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 311 | LD_RUNPATH_SEARCH_PATHS = ( 312 | "$(inherited)", 313 | "@executable_path/Frameworks", 314 | ); 315 | OTHER_LDFLAGS = ( 316 | "$(inherited)", 317 | "-framework", 318 | composeApp, 319 | ); 320 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 321 | PRODUCT_NAME = "${APP_NAME}"; 322 | PROVISIONING_PROFILE_SPECIFIER = ""; 323 | SWIFT_VERSION = 5.0; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | }; 326 | name = Debug; 327 | }; 328 | 7555FFA7242A565B00829871 /* Release */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 332 | CODE_SIGN_IDENTITY = "Apple Development"; 333 | CODE_SIGN_STYLE = Automatic; 334 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 335 | DEVELOPMENT_TEAM = NT77748GS8; 336 | ENABLE_PREVIEWS = YES; 337 | FRAMEWORK_SEARCH_PATHS = ( 338 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 339 | ); 340 | INFOPLIST_FILE = iosApp/Info.plist; 341 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 342 | LD_RUNPATH_SEARCH_PATHS = ( 343 | "$(inherited)", 344 | "@executable_path/Frameworks", 345 | ); 346 | OTHER_LDFLAGS = ( 347 | "$(inherited)", 348 | "-framework", 349 | composeApp, 350 | ); 351 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 352 | PRODUCT_NAME = "${APP_NAME}"; 353 | PROVISIONING_PROFILE_SPECIFIER = ""; 354 | SWIFT_VERSION = 5.0; 355 | TARGETED_DEVICE_FAMILY = "1,2"; 356 | }; 357 | name = Release; 358 | }; 359 | /* End XCBuildConfiguration section */ 360 | 361 | /* Begin XCConfigurationList section */ 362 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 363 | isa = XCConfigurationList; 364 | buildConfigurations = ( 365 | 7555FFA3242A565B00829871 /* Debug */, 366 | 7555FFA4242A565B00829871 /* Release */, 367 | ); 368 | defaultConfigurationIsVisible = 0; 369 | defaultConfigurationName = Release; 370 | }; 371 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 372 | isa = XCConfigurationList; 373 | buildConfigurations = ( 374 | 7555FFA6242A565B00829871 /* Debug */, 375 | 7555FFA7242A565B00829871 /* Release */, 376 | ); 377 | defaultConfigurationIsVisible = 0; 378 | defaultConfigurationName = Release; 379 | }; 380 | /* End XCConfigurationList section */ 381 | }; 382 | rootObject = 7555FF73242A565900829871 /* Project object */; 383 | } 384 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/joreilly.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/joreilly.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/xcuserdata/joreilly.xcuserdatad/xcschemes/iosApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 14 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 41 | 43 | 49 | 50 | 51 | 52 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/xcuserdata/joreilly.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iosApp.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "GeminiKMP" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | google() 8 | gradlePluginPortal() 9 | mavenCentral() 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 18 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") 19 | } 20 | } 21 | 22 | include(":composeApp") 23 | include(":wearApp") -------------------------------------------------------------------------------- /wearApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import java.util.Properties 4 | 5 | plugins { 6 | alias(libs.plugins.androidApplication) 7 | kotlin("android") 8 | alias(libs.plugins.kotlinx.serialization) 9 | alias(libs.plugins.compose.compiler) 10 | } 11 | 12 | android { 13 | compileSdk = 35 14 | defaultConfig { 15 | applicationId = "dev.johnoreilly.gemini" 16 | minSdk = 30 17 | targetSdk = 33 18 | versionCode = 1 19 | versionName = "1.0" 20 | 21 | val localPropsFile = rootProject.file("local.properties") 22 | val localProperties = Properties() 23 | if (localPropsFile.exists()) { 24 | runCatching { 25 | localProperties.load(localPropsFile.inputStream()) 26 | }.getOrElse { 27 | it.printStackTrace() 28 | } 29 | } 30 | 31 | buildConfigField( 32 | "String", 33 | "GEMINI_API_KEY", 34 | "\"" + localProperties["GEMINI_API_KEY"] + "\"", 35 | ) 36 | } 37 | 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_1_8 40 | targetCompatibility = JavaVersion.VERSION_1_8 41 | isCoreLibraryDesugaringEnabled = true 42 | } 43 | 44 | buildFeatures { 45 | compose = true 46 | buildConfig = true 47 | } 48 | 49 | testOptions { 50 | unitTests { 51 | isIncludeAndroidResources = true 52 | } 53 | } 54 | 55 | kotlinOptions { 56 | this.jvmTarget = "1.8" 57 | freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 58 | freeCompilerArgs += "-opt-in=com.google.android.horologist.annotations.ExperimentalHorologistApi" 59 | } 60 | 61 | packaging { 62 | resources { 63 | excludes += listOf( 64 | "/META-INF/AL2.0", 65 | "/META-INF/LGPL2.1", 66 | "/META-INF/versions/**" 67 | ) 68 | } 69 | } 70 | 71 | namespace = "dev.johnoreilly.gemini" 72 | } 73 | 74 | kotlin { 75 | sourceSets.all { 76 | languageSettings { 77 | optIn("kotlin.RequiresOptIn") 78 | } 79 | } 80 | } 81 | 82 | dependencies { 83 | implementation(libs.androidx.activity.compose) 84 | implementation(libs.androidx.wear.compose.navigation) 85 | 86 | implementation(libs.compose.foundation) 87 | implementation(libs.horologist.composables) 88 | implementation(libs.horologist.compose.layout) 89 | implementation(libs.horologist.ai.ui) 90 | implementation(libs.markdown.renderer.core) 91 | implementation(libs.compose.material.iconsext) 92 | implementation(libs.kotlinx.coroutines.core) 93 | implementation(libs.generativeai) 94 | coreLibraryDesugaring(libs.desugar) 95 | } 96 | -------------------------------------------------------------------------------- /wearApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/common/GeminiApi.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.common 2 | 3 | import dev.johnoreilly.gemini.BuildConfig 4 | import dev.shreyaspatil.ai.client.generativeai.GenerativeModel 5 | import dev.shreyaspatil.ai.client.generativeai.type.GenerateContentResponse 6 | 7 | class GeminiApi { 8 | val generativeModel = GenerativeModel( 9 | modelName = "gemini-pro", 10 | apiKey = BuildConfig.GEMINI_API_KEY 11 | ) 12 | 13 | suspend fun generateContent(prompt: String): GenerateContentResponse { 14 | return generativeModel.generateContent(prompt) 15 | } 16 | } -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.wear 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | 7 | class MainActivity : ComponentActivity() { 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | 11 | setContent { 12 | WearApp() 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/Screen.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.wear 2 | 3 | sealed class Screen( 4 | val route: String, 5 | ) { 6 | object PromptScreen : Screen("prompt") 7 | } -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/WearApp.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.wear 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.navigation.NavHostController 6 | import androidx.wear.compose.navigation.SwipeDismissableNavHost 7 | import androidx.wear.compose.navigation.composable 8 | import androidx.wear.compose.navigation.rememberSwipeDismissableNavController 9 | import com.google.android.horologist.compose.layout.AppScaffold 10 | import dev.johnoreilly.gemini.wear.prompt.GeminiPromptScreen 11 | 12 | 13 | @Composable 14 | fun WearApp( 15 | modifier: Modifier = Modifier, 16 | navController: NavHostController = rememberSwipeDismissableNavController(), 17 | ) { 18 | AppScaffold(modifier = modifier) { 19 | SwipeDismissableNavHost( 20 | startDestination = Screen.PromptScreen.route, 21 | navController = navController, 22 | ) { 23 | composable( 24 | route = Screen.PromptScreen.route, 25 | ) { 26 | GeminiPromptScreen() 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/markdown/WearMaterialTypography.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.wear.markdown 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.text.SpanStyle 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontStyle 8 | import androidx.wear.compose.material.LocalContentColor 9 | import androidx.wear.compose.material.MaterialTheme 10 | import com.mikepenz.markdown.model.DefaultMarkdownColors 11 | import com.mikepenz.markdown.model.DefaultMarkdownTypography 12 | 13 | @Composable 14 | fun wearMaterialTypography() = DefaultMarkdownTypography( 15 | h1 = MaterialTheme.typography.title1, 16 | h2 = MaterialTheme.typography.title2, 17 | h3 = MaterialTheme.typography.title3, 18 | h4 = MaterialTheme.typography.caption1, 19 | h5 = MaterialTheme.typography.caption2, 20 | h6 = MaterialTheme.typography.caption3, 21 | text = MaterialTheme.typography.body1, 22 | code = MaterialTheme.typography.body2.copy(fontFamily = FontFamily.Monospace), 23 | quote = MaterialTheme.typography.body2.plus(SpanStyle(fontStyle = FontStyle.Italic)), 24 | paragraph = MaterialTheme.typography.body1, 25 | ordered = MaterialTheme.typography.body1, 26 | bullet = MaterialTheme.typography.body1, 27 | list = MaterialTheme.typography.body1, 28 | inlineCode = MaterialTheme.typography.body1, 29 | link = MaterialTheme.typography.body1 30 | ) 31 | 32 | @Composable 33 | fun wearMaterialColors() = DefaultMarkdownColors( 34 | text = Color.White, 35 | codeText = LocalContentColor.current, 36 | linkText = Color.Blue, 37 | codeBackground = MaterialTheme.colors.background, 38 | inlineCodeBackground = MaterialTheme.colors.background, 39 | dividerColor = MaterialTheme.colors.secondaryVariant, 40 | inlineCodeText = MaterialTheme.colors.primary 41 | ) -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/prompt/GeminiPromptScreen.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.wear.prompt 2 | 3 | import android.content.Intent 4 | import android.speech.RecognizerIntent 5 | import androidx.activity.compose.rememberLauncherForActivityResult 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Mic 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 17 | import androidx.lifecycle.viewmodel.compose.viewModel 18 | import androidx.wear.compose.material.Card 19 | import androidx.wear.compose.material.CardDefaults 20 | import androidx.wear.compose.material.MaterialTheme 21 | import com.google.android.horologist.ai.ui.components.PromptOrResponseDisplay 22 | import com.google.android.horologist.ai.ui.model.PromptOrResponseUiModel 23 | import com.google.android.horologist.ai.ui.model.TextResponseUiModel 24 | import com.google.android.horologist.ai.ui.screens.PromptScreen 25 | import com.google.android.horologist.ai.ui.screens.PromptUiState 26 | import com.google.android.horologist.compose.layout.ScalingLazyColumnState 27 | import com.google.android.horologist.compose.layout.ScreenScaffold 28 | import com.google.android.horologist.compose.layout.rememberColumnState 29 | import com.google.android.horologist.compose.material.Button 30 | import com.mikepenz.markdown.compose.Markdown 31 | import dev.johnoreilly.gemini.R 32 | import dev.johnoreilly.gemini.wear.markdown.wearMaterialColors 33 | import dev.johnoreilly.gemini.wear.markdown.wearMaterialTypography 34 | 35 | @Composable 36 | fun GeminiPromptScreen( 37 | modifier: Modifier = Modifier, 38 | viewModel: GeminiPromptViewModel = viewModel(), 39 | columnState: ScalingLazyColumnState = rememberColumnState() 40 | ) { 41 | val uiState by viewModel.uiState.collectAsStateWithLifecycle() 42 | 43 | val voiceLauncher = 44 | rememberLauncherForActivityResult( 45 | ActivityResultContracts.StartActivityForResult(), 46 | ) { 47 | it.data?.let { data -> 48 | val results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) 49 | val enteredPrompt = results?.get(0) 50 | if (!enteredPrompt.isNullOrBlank()) { 51 | viewModel.askQuestion(enteredPrompt) 52 | } 53 | } 54 | } 55 | 56 | val voiceIntent: Intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { 57 | putExtra( 58 | RecognizerIntent.EXTRA_LANGUAGE_MODEL, 59 | RecognizerIntent.LANGUAGE_MODEL_FREE_FORM, 60 | ) 61 | 62 | putExtra( 63 | RecognizerIntent.EXTRA_PROMPT, 64 | stringResource(R.string.prompt_input), 65 | ) 66 | } 67 | 68 | GeminiPromptScreen( 69 | uiState = uiState, 70 | modifier = modifier, 71 | columnState = columnState, 72 | ) { 73 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { 74 | Button( 75 | Icons.Default.Mic, 76 | contentDescription = stringResource(R.string.prompt_input), 77 | onClick = { 78 | voiceLauncher.launch(voiceIntent) 79 | }, 80 | ) 81 | } 82 | } 83 | } 84 | 85 | @Composable 86 | private fun GeminiPromptScreen( 87 | uiState: PromptUiState, 88 | modifier: Modifier = Modifier, 89 | columnState: ScalingLazyColumnState = rememberColumnState(), 90 | promptEntry: @Composable () -> Unit, 91 | ) { 92 | ScreenScaffold(scrollState = columnState) { 93 | PromptScreen( 94 | uiState = uiState, 95 | modifier = modifier, 96 | promptEntry = promptEntry, 97 | promptDisplay = { GeminiPromptDisplay(it) } 98 | ) 99 | } 100 | } 101 | 102 | @Composable 103 | private fun GeminiPromptDisplay(it: PromptOrResponseUiModel) { 104 | if (it is TextResponseUiModel) { 105 | GeminiTextResponseCard(it) 106 | } else { 107 | PromptOrResponseDisplay( 108 | promptResponse = it, 109 | onClick = {}, 110 | ) 111 | } 112 | } 113 | 114 | @Composable 115 | fun GeminiTextResponseCard( 116 | textResponseUiModel: TextResponseUiModel, 117 | modifier: Modifier = Modifier, 118 | onClick: () -> Unit = {}, 119 | ) { 120 | Card( 121 | modifier = modifier.fillMaxWidth(), 122 | onClick = onClick, 123 | backgroundPainter = CardDefaults.cardBackgroundPainter( 124 | MaterialTheme.colors.surface, 125 | MaterialTheme.colors.surface, 126 | ), 127 | ) { 128 | Markdown( 129 | textResponseUiModel.text, 130 | colors = wearMaterialColors(), 131 | typography = wearMaterialTypography(), 132 | ) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /wearApp/src/main/kotlin/dev/johnoreilly/gemini/wear/prompt/GeminiPromptViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.johnoreilly.gemini.wear.prompt 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.google.android.horologist.ai.ui.model.FailedResponseUiModel 6 | import com.google.android.horologist.ai.ui.model.ModelInstanceUiModel 7 | import com.google.android.horologist.ai.ui.model.PromptOrResponseUiModel 8 | import com.google.android.horologist.ai.ui.model.TextPromptUiModel 9 | import com.google.android.horologist.ai.ui.model.TextResponseUiModel 10 | import com.google.android.horologist.ai.ui.screens.PromptUiState 11 | import dev.johnoreilly.gemini.common.GeminiApi 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.SharingStarted 14 | import kotlinx.coroutines.flow.StateFlow 15 | import kotlinx.coroutines.flow.combine 16 | import kotlinx.coroutines.flow.stateIn 17 | import kotlinx.coroutines.flow.update 18 | import kotlinx.coroutines.launch 19 | 20 | class GeminiPromptViewModel : ViewModel() { 21 | val api = GeminiApi() 22 | 23 | val previousQuestions: MutableStateFlow> = 24 | MutableStateFlow(listOf()) 25 | val pendingQuestion: MutableStateFlow = 26 | MutableStateFlow(null) 27 | 28 | fun askQuestion(enteredPrompt: String) { 29 | val textPromptUiModel = TextPromptUiModel(enteredPrompt) 30 | pendingQuestion.value = textPromptUiModel 31 | 32 | viewModelScope.launch { 33 | val responseUi = queryForPrompt(enteredPrompt) 34 | 35 | previousQuestions.update { 36 | it + listOf(textPromptUiModel, responseUi) 37 | } 38 | 39 | pendingQuestion.value = null 40 | } 41 | } 42 | 43 | private suspend fun queryForPrompt( 44 | enteredPrompt: String, 45 | ): PromptOrResponseUiModel { 46 | return try { 47 | val text = api.generateContent(enteredPrompt).text ?: error("No results") 48 | TextResponseUiModel(text) 49 | } catch (e: Exception) { 50 | FailedResponseUiModel(e.toString()) 51 | } 52 | } 53 | 54 | val uiState: StateFlow = 55 | combine( 56 | previousQuestions, 57 | pendingQuestion, 58 | ) { prev, curr -> 59 | val modelInfo = ModelInstanceUiModel("gemini", "Gemini") 60 | PromptUiState(modelInfo, prev, curr) 61 | }.stateIn( 62 | viewModelScope, 63 | started = SharingStarted.WhileSubscribed(5000), 64 | initialValue = PromptUiState(messages = previousQuestions.value), 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /wearApp/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /wearApp/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joreilly/GeminiKMP/fe9035602562bf512c556c2a9d0c29fc29801806/wearApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /wearApp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GeminiKMP 3 | Voice Prompt 4 | --------------------------------------------------------------------------------