diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 098b5d20..28afd68e 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -20,16 +20,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' cache: gradle - name: Build the generativeai release artifacts run: ./gradlew generativeai:publishAllPublicationsToMavenRepository - - name: Upload the generativeai artifacts - uses: actions/upload-artifact@v2 + + - name: Upload generated artifacts + uses: actions/upload-artifact@v4 + with: name: generative-ai-android path: generativeai/m2 diff --git a/.github/workflows/check-code-format.yml b/.github/workflows/check-code-format.yml index c96607eb..906f3265 100644 --- a/.github/workflows/check-code-format.yml +++ b/.github/workflows/check-code-format.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout branch - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/check-licensing.yml b/.github/workflows/check-licensing.yml index 19f2c270..d1e3e555 100644 --- a/.github/workflows/check-licensing.yml +++ b/.github/workflows/check-licensing.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout branch - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/check_for_api_changes.yml b/.github/workflows/check_for_api_changes.yml index 726a8b0b..c9aa4269 100644 --- a/.github/workflows/check_for_api_changes.yml +++ b/.github/workflows/check_for_api_changes.yml @@ -7,12 +7,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout master - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4 with: ref: ${{ github.base_ref }} - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin @@ -30,10 +30,10 @@ jobs: mv common/public.api ~/common/public.api - name: Checkout branch - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin diff --git a/.github/workflows/generate_docs.yml b/.github/workflows/generate_docs.yml index a5525910..2bda60d0 100644 --- a/.github/workflows/generate_docs.yml +++ b/.github/workflows/generate_docs.yml @@ -20,8 +20,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' @@ -29,7 +29,7 @@ jobs: - name: Run dokka run: ./gradlew generativeai:dokkaHtml - name: Upload generated docs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: google-ai-android path: generativeai/build/dokka/html diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 2ddafab8..f20f3f7e 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -7,9 +7,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout branch - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin diff --git a/generativeai-android-sample/app/build.gradle.kts b/generativeai-android-sample/app/build.gradle.kts index f9d494d4..aeb4c423 100644 --- a/generativeai-android-sample/app/build.gradle.kts +++ b/generativeai-android-sample/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + /* * Copyright 2023 Google LLC * @@ -50,14 +52,33 @@ android { composeOptions { kotlinCompilerExtensionVersion = "1.5.4" } + afterEvaluate { + val projectPath = rootProject.file(".").absolutePath + tasks.withType { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + listOf( + "-opt-in=kotlin.OptIn", + "-opt-in=kotlin.RequiresOptIn", + ) + freeCompilerArgs = freeCompilerArgs + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$projectPath/report/compose-metrics", + ) + freeCompilerArgs = freeCompilerArgs + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$projectPath/report/compose-reports", + ) + } + } + } } dependencies { - implementation("androidx.core:core-ktx:1.9.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") - implementation("androidx.activity:activity-compose:1.8.1") - implementation("androidx.navigation:navigation-compose:2.7.5") + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation("androidx.navigation:navigation-compose:2.7.6") implementation(platform("androidx.compose:compose-bom:2023.10.01")) implementation("androidx.compose.ui:ui") diff --git a/generativeai-android-sample/app/src/main/AndroidManifest.xml b/generativeai-android-sample/app/src/main/AndroidManifest.xml index 0d795b20..0e5414a8 100644 --- a/generativeai-android-sample/app/src/main/AndroidManifest.xml +++ b/generativeai-android-sample/app/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ + android:windowSoftInputMode="adjustResize"> diff --git a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt index 241fa595..b045c0bd 100644 --- a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt +++ b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:Suppress("UNCHECKED_CAST") + package com.google.ai.sample import androidx.lifecycle.ViewModel @@ -27,14 +29,14 @@ import com.google.ai.sample.feature.text.SummarizeViewModel val GenerativeViewModelFactory = object : ViewModelProvider.Factory { override fun create( - viewModelClass: Class, + modelClass: Class, extras: CreationExtras ): T { val config = generationConfig { temperature = 0.7f } - return with(viewModelClass) { + return with(modelClass) { when { isAssignableFrom(SummarizeViewModel::class.java) -> { // Initialize a GenerativeModel with the `gemini-flash` AI model @@ -69,7 +71,7 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory { } else -> - throw IllegalArgumentException("Unknown ViewModel class: ${viewModelClass.name}") + throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } as T } diff --git a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt index ce76394b..118987c1 100644 --- a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt +++ b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatMessage.kt @@ -24,7 +24,6 @@ enum class Participant { data class ChatMessage( val id: String = UUID.randomUUID().toString(), - var text: String = "", + val text: String = "", val participant: Participant = Participant.USER, - var isPending: Boolean = false ) diff --git a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt index d18846c3..6a40fe54 100644 --- a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt +++ b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatScreen.kt @@ -72,6 +72,7 @@ internal fun ChatRoute( Scaffold( bottomBar = { MessageInput( + isLoading = chatUiState.isLoading, onSendMessage = { inputText -> chatViewModel.sendMessage(inputText) }, @@ -146,13 +147,6 @@ fun ChatBubbleItem( modifier = Modifier.padding(bottom = 4.dp) ) Row { - if (chatMessage.isPending) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(all = 8.dp) - ) - } BoxWithConstraints { Card( colors = CardDefaults.cardColors(containerColor = backgroundColor), @@ -171,6 +165,7 @@ fun ChatBubbleItem( @Composable fun MessageInput( + isLoading: Boolean, onSendMessage: (String) -> Unit, resetScroll: () -> Unit = {} ) { @@ -198,6 +193,7 @@ fun MessageInput( .weight(0.85f) ) IconButton( + enabled = !isLoading, onClick = { if (userMessage.isNotBlank()) { onSendMessage(userMessage) @@ -211,11 +207,19 @@ fun MessageInput( .fillMaxWidth() .weight(0.15f) ) { - Icon( - Icons.Default.Send, - contentDescription = stringResource(R.string.action_send), - modifier = Modifier - ) + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(all = 8.dp) + ) + } else { + Icon( + Icons.Default.Send, + contentDescription = stringResource(R.string.action_send), + modifier = Modifier + ) + } } } } diff --git a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt index 48dea90e..fccc8c08 100644 --- a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt +++ b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatUiState.kt @@ -16,24 +16,7 @@ package com.google.ai.sample.feature.chat -import androidx.compose.runtime.toMutableStateList - -class ChatUiState( - messages: List = emptyList() -) { - private val _messages: MutableList = messages.toMutableStateList() - val messages: List = _messages - - fun addMessage(msg: ChatMessage) { - _messages.add(msg) - } - - fun replaceLastPendingMessage() { - val lastMessage = _messages.lastOrNull() - lastMessage?.let { - val newMessage = lastMessage.apply { isPending = false } - _messages.removeLast() - _messages.add(newMessage) - } - } -} +data class ChatUiState( + val isLoading : Boolean, + val messages: List +) \ No newline at end of file diff --git a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt index f102678d..d1c8543b 100644 --- a/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt +++ b/generativeai-android-sample/app/src/main/kotlin/com/google/ai/sample/feature/chat/ChatViewModel.kt @@ -21,10 +21,14 @@ import androidx.lifecycle.viewModelScope import com.google.ai.client.generativeai.GenerativeModel import com.google.ai.client.generativeai.type.asTextOrNull import com.google.ai.client.generativeai.type.content +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException class ChatViewModel( generativeModel: GenerativeModel @@ -36,52 +40,62 @@ class ChatViewModel( ) ) - private val _uiState: MutableStateFlow = - MutableStateFlow(ChatUiState(chat.history.map { content -> - // Map the initial messages - ChatMessage( - text = content.parts.first().asTextOrNull() ?: "", - participant = if (content.role == "user") Participant.USER else Participant.MODEL, - isPending = false - ) - })) - val uiState: StateFlow = - _uiState.asStateFlow() + private val _uiState: MutableStateFlow = MutableStateFlow( + ChatUiState( + isLoading = false, + messages = initMessage(), + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + private fun initMessage(): List = chat.history.map { content -> + // Map the initial messages + ChatMessage( + text = content.parts.first().asTextOrNull() ?: "", + participant = if (content.role == "user") Participant.USER else Participant.MODEL, + ) + } fun sendMessage(userMessage: String) { - // Add a pending message - _uiState.value.addMessage( - ChatMessage( - text = userMessage, - participant = Participant.USER, - isPending = true + // Loading state, update user message + _uiState.update { currentState -> + currentState.copy( + isLoading = true, + messages = currentState.messages + ChatMessage( + text = userMessage, + participant = Participant.USER, + ) ) - ) + } viewModelScope.launch { try { - val response = chat.sendMessage(userMessage) - - _uiState.value.replaceLastPendingMessage() - - response.text?.let { modelResponse -> - _uiState.value.addMessage( - ChatMessage( + val response = withContext(Dispatchers.IO) { + chat.sendMessage(userMessage) + } + val modelResponse = response.text ?: return@launch + _uiState.update { + it.copy( + isLoading = false, + messages = it.messages + ChatMessage( text = modelResponse, participant = Participant.MODEL, - isPending = false ) ) } - } catch (e: Exception) { - _uiState.value.replaceLastPendingMessage() - _uiState.value.addMessage( - ChatMessage( - text = e.localizedMessage, - participant = Participant.ERROR + } catch (cancel: CancellationException) { + throw cancel + } catch (throwable: Throwable) { + _uiState.update { + it.copy( + isLoading = false, + messages = it.messages + ChatMessage( + text = throwable.localizedMessage ?: "Error", + participant = Participant.ERROR, + ) ) - ) + } } } }