From 2de413740cd4b02487837c395cb4c595b26f00e3 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Wed, 26 Apr 2023 16:30:19 -0400 Subject: [PATCH 1/7] feat.: Impl. Usage Tracking for Image Generation --- .../services/implementation/OpenAiService.kt | 17 +- .../botforge/image/model/ImageSizeInternal.kt | 6 +- .../service/SharedPreferencesService.kt | 11 +- .../SharedPreferencesServiceImpl.kt | 72 ++++++++- .../botforge/settings/ui/ApiUsageUi.kt | 152 +++++++++++++++++- .../settings/viewmodel/SettingsViewModel.kt | 18 ++- app/src/main/res/values/numbers.xml | 5 +- app/src/main/res/values/strings.xml | 8 +- 8 files changed, 266 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt b/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt index 9e847a9..2e12337 100644 --- a/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt +++ b/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt @@ -15,8 +15,8 @@ import com.aallam.openai.client.OpenAI import com.mohandass.botforge.chat.model.Message import com.mohandass.botforge.chat.model.MessageMetadata import com.mohandass.botforge.chat.model.Role -import com.mohandass.botforge.common.services.OpenAiService import com.mohandass.botforge.common.services.Logger +import com.mohandass.botforge.common.services.OpenAiService import com.mohandass.botforge.settings.service.SharedPreferencesService /** @@ -101,6 +101,21 @@ class OpenAiServiceImpl private constructor( ) ) + // Update usage image count + when (imageSize) { + ImageSize.is256x256 -> { + sharedPreferencesService.incrementUsageImageSmallCount(n) + } + + ImageSize.is512x512 -> { + sharedPreferencesService.incrementUsageImageMediumCount(n) + } + + ImageSize.is1024x1024 -> { + sharedPreferencesService.incrementUsageImageLargeCount(n) + } + } + logger.logVerbose(TAG, "generateImage() $images") return images } catch (e: Exception) { diff --git a/app/src/main/java/com/mohandass/botforge/image/model/ImageSizeInternal.kt b/app/src/main/java/com/mohandass/botforge/image/model/ImageSizeInternal.kt index 9d018d5..7f55efd 100644 --- a/app/src/main/java/com/mohandass/botforge/image/model/ImageSizeInternal.kt +++ b/app/src/main/java/com/mohandass/botforge/image/model/ImageSizeInternal.kt @@ -25,9 +25,9 @@ class ImageSizeInternal(size: ImageSize) { override fun toString(): String { return when (this) { - is256x256 -> "256x256" - is512x512 -> "512x512" - is1024x1024 -> "1024x1024" + is256x256 -> "Small (256x256)" + is512x512 -> "Medium (512x512)" + is1024x1024 -> "Large (1024x1024)" else -> { throw IllegalArgumentException("Unknown image size: $this") } diff --git a/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt b/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt index 1017d2f..537c31a 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt @@ -19,8 +19,14 @@ interface SharedPreferencesService { // Usage fun getUsageTokens(): Long + fun getUsageImageSmallCount(): Long + fun getUsageImageMediumCount(): Long + fun getUsageImageLargeCount(): Long fun incrementUsageTokens(tokens: Int) - fun resetUsageTokens() + fun incrementUsageImageSmallCount(count: Int) + fun incrementUsageImageMediumCount(count: Int) + fun incrementUsageImageLargeCount(count: Int) + fun resetUsage() // Analytics fun getAnalyticsOptOut(): Boolean @@ -30,6 +36,9 @@ interface SharedPreferencesService { const val USER_PREFERENCES_NAME = "user_preferences" const val API_KEY = "open_ai_api_key" const val API_USAGE_AS_TOKENS = "open_ai_api_usage_tokens" + const val API_USAGE_IMAGE_SMALL_COUNT = "open_ai_api_usage_image_small_count" + const val API_USAGE_IMAGE_MEDIUM_COUNT = "open_ai_api_usage_image_medium_count" + const val API_USAGE_IMAGE_LARGE_COUNT = "open_ai_api_usage_image_large_count" const val ON_BOARDING_COMPLETED = "on_boarding_completed" const val ANALYTICS_OPT_OUT = "analytics_opt_out" } diff --git a/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt b/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt index e6a2758..0382347 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt @@ -39,24 +39,90 @@ class SharedPreferencesServiceImpl private constructor(context: Context) : } } + private val _usageTokens: Long + get() { + return sharedPreferences.getLong(SharedPreferencesService.API_USAGE_AS_TOKENS, 0) + } + override fun getUsageTokens(): Long { return _usageTokens } - private val _usageTokens: Long + private val _usageImageSmallCount: Long get() { - return sharedPreferences.getLong(SharedPreferencesService.API_USAGE_AS_TOKENS, 0) + return sharedPreferences.getLong( + SharedPreferencesService.API_USAGE_IMAGE_SMALL_COUNT, + 0 + ) } + override fun getUsageImageSmallCount(): Long { + return _usageImageSmallCount + } + + private val _usageImageMediumCount: Long + get() { + return sharedPreferences.getLong( + SharedPreferencesService.API_USAGE_IMAGE_MEDIUM_COUNT, + 0 + ) + } + + override fun getUsageImageMediumCount(): Long { + return _usageImageMediumCount + } + + private val _usageImageLargeCount: Long + get() { + return sharedPreferences.getLong( + SharedPreferencesService.API_USAGE_IMAGE_LARGE_COUNT, + 0 + ) + } + + override fun getUsageImageLargeCount(): Long { + return _usageImageLargeCount + } + override fun incrementUsageTokens(tokens: Int) { sharedPreferences.edit { putLong(SharedPreferencesService.API_USAGE_AS_TOKENS, _usageTokens + tokens) } } - override fun resetUsageTokens() { + override fun incrementUsageImageSmallCount(count: Int) { + sharedPreferences.edit { + putLong( + SharedPreferencesService.API_USAGE_IMAGE_SMALL_COUNT, + _usageImageSmallCount + count + ) + } + } + + override fun incrementUsageImageMediumCount(count: Int) { + sharedPreferences.edit { + putLong( + SharedPreferencesService.API_USAGE_IMAGE_MEDIUM_COUNT, + _usageImageMediumCount + count + ) + } + } + + override fun incrementUsageImageLargeCount(count: Int) { + sharedPreferences.edit { + putLong( + SharedPreferencesService.API_USAGE_IMAGE_LARGE_COUNT, + _usageImageLargeCount + count + ) + } + } + + override fun resetUsage() { sharedPreferences.edit { putLong(SharedPreferencesService.API_USAGE_AS_TOKENS, 0) + putLong(SharedPreferencesService.API_USAGE_IMAGE_SMALL_COUNT, 0) + putLong(SharedPreferencesService.API_USAGE_IMAGE_MEDIUM_COUNT, 0) + putLong(SharedPreferencesService.API_USAGE_IMAGE_LARGE_COUNT, 0) } } diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt index 64f6815..f503032 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -41,6 +42,7 @@ import com.mohandass.botforge.resources import com.mohandass.botforge.settings.ui.components.SettingsCategory import com.mohandass.botforge.settings.ui.components.SettingsItem import com.mohandass.botforge.settings.viewmodel.SettingsViewModel +import com.slaviboy.composeunits.adw import java.text.DecimalFormat @Composable @@ -49,6 +51,11 @@ fun ApiUsageUi( ) { val showTokenInfoDialog = remember { mutableStateOf(false) } + val usageTokens by settingsViewModel.usageTokens + val usageImageSmallCount by settingsViewModel.usageImageSmallCount + val usageImageMediumCount by settingsViewModel.usageImageMediumCount + val usageImageLargeCount by settingsViewModel.usageImageLargeCount + val context = LocalContext.current if (showTokenInfoDialog.value) { @@ -141,6 +148,19 @@ fun ApiUsageUi( Text( text = resources().getString(R.string.your_usage), modifier = Modifier.padding(start = 10.dp), + style = MaterialTheme.typography.titleMedium + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + + Spacer(modifier = Modifier.width(0.05.adw)) + + Text( + text = stringResource(R.string.tokens), + modifier = Modifier.padding(start = 10.dp), style = MaterialTheme.typography.labelLarge ) @@ -171,16 +191,18 @@ fun ApiUsageUi( Spacer(modifier = Modifier.width(4.dp)) Text( - text = settingsViewModel.getUsageTokens().toString(), - modifier = Modifier.padding(10.dp), + text = usageTokens.toString(), style = MaterialTheme.typography.bodyMedium ) - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(0.05.adw)) } // Show cost only if API level is 29+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + + Spacer(modifier = Modifier.height(15.dp)) + Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -199,7 +221,7 @@ fun ApiUsageUi( text = DecimalFormat("0.0000") .format( resources().getFloat(R.dimen.gpt_3_5_turbo_cost_per_1k_tokens) * - settingsViewModel.getUsageTokens().div(1000) + usageTokens.div(1000) ).toString(), modifier = Modifier.padding(end = 10.dp), style = MaterialTheme.typography.labelMedium, @@ -212,7 +234,123 @@ fun ApiUsageUi( } } - Spacer(modifier = Modifier.height(15.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + + Spacer(modifier = Modifier.width(0.05.adw)) + + Text( + text = stringResource(id = R.string.image_generation), + modifier = Modifier.padding(start = 10.dp), + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.weight(1f)) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + + Spacer(modifier = Modifier.width(0.1.adw)) + + Text( + text = stringResource(id = R.string.image_small), + modifier = Modifier.padding(start = 10.dp), + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = usageImageSmallCount.toString(), + modifier = Modifier.padding(end = 10.dp), + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.width(0.05.adw)) + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + + Spacer(modifier = Modifier.width(0.1.adw)) + + Text( + text = stringResource(id = R.string.image_medium), + modifier = Modifier.padding(start = 10.dp), + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = usageImageMediumCount.toString(), + modifier = Modifier.padding(end = 10.dp), + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.width(0.05.adw)) + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + + Spacer(modifier = Modifier.width(0.1.adw)) + + Text( + text = stringResource(id = R.string.image_large), + modifier = Modifier.padding(start = 10.dp), + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = usageImageLargeCount.toString(), + modifier = Modifier.padding(end = 10.dp), + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.width(0.05.adw)) + } + + // Show cost only if API level is 29+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.weight(1f)) + + Icon( + painter = painterResource(id = R.drawable.baseline_attach_money_24), + contentDescription = stringResource(id = R.string.dollars), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = DecimalFormat("0.0000") + .format( + (resources().getFloat(R.dimen.dall_e_256) * usageImageSmallCount) + + (resources().getFloat(R.dimen.dall_e_512) * usageImageMediumCount) + + (resources().getFloat(R.dimen.dall_e_1024) * usageImageLargeCount) + ).toString(), + modifier = Modifier.padding(end = 10.dp), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.error + ) + + Spacer(modifier = Modifier.width(10.dp)) + } + } Row { Spacer(modifier = Modifier.weight(1f)) @@ -220,7 +358,7 @@ fun ApiUsageUi( TextButton( modifier = Modifier.padding(horizontal = 10.dp), onClick = { - settingsViewModel.resetUsageTokens() + settingsViewModel.resetUsage() } ) { Text( @@ -256,4 +394,4 @@ fun ApiUsageUi( context.startActivity(intent) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/mohandass/botforge/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/mohandass/botforge/settings/viewmodel/SettingsViewModel.kt index df24b48..e3225a0 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/viewmodel/SettingsViewModel.kt @@ -4,6 +4,7 @@ package com.mohandass.botforge.settings.viewmodel +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.auth.AuthCredential @@ -56,15 +57,20 @@ class SettingsViewModel @Inject constructor( fun getDisplayName() = accountService.displayName // Usage - fun getUsageTokens(): Long { - logger.logVerbose(TAG, "getUsageTokens()") - return sharedPreferencesService.getUsageTokens() - } + val usageTokens = mutableStateOf(sharedPreferencesService.getUsageTokens()) + val usageImageSmallCount = mutableStateOf(sharedPreferencesService.getUsageImageSmallCount()) + val usageImageMediumCount = mutableStateOf(sharedPreferencesService.getUsageImageMediumCount()) + val usageImageLargeCount = mutableStateOf(sharedPreferencesService.getUsageImageLargeCount()) - fun resetUsageTokens() { + fun resetUsage() { logger.log(TAG, "resetUsageTokens()") - sharedPreferencesService.resetUsageTokens() + sharedPreferencesService.resetUsage() SnackbarManager.showMessage(R.string.usage_tokens_reset) + + usageTokens.value = sharedPreferencesService.getUsageTokens() + usageImageSmallCount.value = sharedPreferencesService.getUsageImageSmallCount() + usageImageMediumCount.value = sharedPreferencesService.getUsageImageMediumCount() + usageImageLargeCount.value = sharedPreferencesService.getUsageImageLargeCount() } fun updateTheme(preferredTheme: PreferredTheme) { diff --git a/app/src/main/res/values/numbers.xml b/app/src/main/res/values/numbers.xml index cd7421f..6656096 100644 --- a/app/src/main/res/values/numbers.xml +++ b/app/src/main/res/values/numbers.xml @@ -1,4 +1,7 @@ - 0.0002 + 0.0002 + 0.020 + 0.018 + 0.016 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82bb581..fcd4ecc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,11 +105,17 @@ OpenAI Pricing "Learn more about OpenAI's pricing" https://openai.com/pricing - Your Usage (Tokens) + Your Usage https://platform.openai.com/account/usage Reset Usage Local token counter reset! + Image Generation + Small (256x256) + Medium (512x512) + Large (1024x1024) + + Delete Account Are you sure you want to delete your account? Account deleted! From 74fffa220f1be2f62356abac25a045691389da96 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Wed, 26 Apr 2023 16:38:22 -0400 Subject: [PATCH 2/7] refac.: Move TokenInfoDialog outside ApiUsageUi as Component --- .../botforge/settings/ui/ApiUsageUi.kt | 59 +------------- .../ui/components/dialogs/TokenInfoDialog.kt | 81 +++++++++++++++++++ 2 files changed, 85 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/mohandass/botforge/settings/ui/components/dialogs/TokenInfoDialog.kt diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt index f503032..270e83f 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt @@ -15,10 +15,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -33,14 +31,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.mohandass.botforge.R import com.mohandass.botforge.resources import com.mohandass.botforge.settings.ui.components.SettingsCategory import com.mohandass.botforge.settings.ui.components.SettingsItem +import com.mohandass.botforge.settings.ui.components.dialogs.TokenInfoDialog import com.mohandass.botforge.settings.viewmodel.SettingsViewModel import com.slaviboy.composeunits.adw import java.text.DecimalFormat @@ -59,56 +55,9 @@ fun ApiUsageUi( val context = LocalContext.current if (showTokenInfoDialog.value) { - AlertDialog(onDismissRequest = { showTokenInfoDialog.value = false }, - title = { - Text(text = stringResource(id = R.string.managing_tokens)) - }, - text = { - Column { - Text(text = stringResource(id = R.string.managing_tokens_message)) - - val annotatedStringManagingTokens = buildAnnotatedString { - withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.onBackground)) { - append(resources().getString(R.string.learn_more_managing_tokens) + " ") - } - addStringAnnotation( - tag = "URL", - annotation = resources().getString(R.string.open_ai_managing_tokens_link), - start = length, - end = length + resources().getString(R.string.open_ai_managing_tokens_link).length - ) - withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { - append(resources().getString(R.string.open_ai_managing_tokens)) - } - } - - ClickableText( - text = annotatedStringManagingTokens, - modifier = Modifier.padding(vertical = 10.dp), - onClick = { offset -> - annotatedStringManagingTokens.getStringAnnotations( - tag = "URL", - start = offset, - end = offset - ) - .firstOrNull()?.let { annotation -> - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(annotation.item) - } - context.startActivity(intent) - } - } - ) - } - }, - confirmButton = { - TextButton(onClick = { - showTokenInfoDialog.value = false - }) { - Text(text = stringResource(id = R.string.dismiss)) - } - }, - dismissButton = {} + TokenInfoDialog( + onDismiss = { showTokenInfoDialog.value = false }, + context = context ) } diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/components/dialogs/TokenInfoDialog.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/components/dialogs/TokenInfoDialog.kt new file mode 100644 index 0000000..35155cd --- /dev/null +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/components/dialogs/TokenInfoDialog.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Dheshan Mohandass (L4TTiCe) +// +// SPDX-License-Identifier: MIT + +package com.mohandass.botforge.settings.ui.components.dialogs + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.mohandass.botforge.R +import com.mohandass.botforge.resources + +@Composable +fun TokenInfoDialog( + onDismiss: () -> Unit, + context: Context +) { + AlertDialog(onDismissRequest = onDismiss, + title = { + Text(text = stringResource(id = R.string.managing_tokens)) + }, + text = { + Column { + Text(text = stringResource(id = R.string.managing_tokens_message)) + + val annotatedStringManagingTokens = buildAnnotatedString { + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.onBackground)) { + append(resources().getString(R.string.learn_more_managing_tokens) + " ") + } + addStringAnnotation( + tag = "URL", + annotation = resources().getString(R.string.open_ai_managing_tokens_link), + start = length, + end = length + resources().getString(R.string.open_ai_managing_tokens_link).length + ) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(resources().getString(R.string.open_ai_managing_tokens)) + } + } + + ClickableText( + text = annotatedStringManagingTokens, + modifier = Modifier.padding(vertical = 10.dp), + onClick = { offset -> + annotatedStringManagingTokens.getStringAnnotations( + tag = "URL", + start = offset, + end = offset + ) + .firstOrNull()?.let { annotation -> + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(annotation.item) + } + context.startActivity(intent) + } + } + ) + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = R.string.dismiss)) + } + }, + dismissButton = {} + ) +} From befaf411bcbe898d5b80286e2ee0e6978e5128c3 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Wed, 26 Apr 2023 18:01:18 -0400 Subject: [PATCH 3/7] feat.: Impl. Image variant generation --- app/build.gradle.kts | 3 + .../botforge/common/services/OpenAiService.kt | 7 ++ .../services/implementation/OpenAiService.kt | 46 ++++++++++++ .../mohandass/botforge/image/ui/ImageUi.kt | 11 +++ .../image/viewmodel/ImageViewModel.kt | 74 ++++++++++++++++++- .../main/res/drawable/baseline_refresh_24.xml | 5 ++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_refresh_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f87ce5b..bd3095d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -118,6 +118,7 @@ dependencies { val coilVersion = "2.3.0" val markwonVersion = "4.6.2" val leakCanaryVersion = "2.10" + val okioVersion = "3.3.0" val playServicesAuthVersion = "20.5.0" @@ -209,6 +210,8 @@ dependencies { // LeakCanary for memory leak detection // https://square.github.io/leakcanary/ debugImplementation("com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion") + + implementation("com.squareup.okio:okio:$okioVersion") } // Dependency Injection with Hilt diff --git a/app/src/main/java/com/mohandass/botforge/common/services/OpenAiService.kt b/app/src/main/java/com/mohandass/botforge/common/services/OpenAiService.kt index 6dc2f31..c0ce649 100644 --- a/app/src/main/java/com/mohandass/botforge/common/services/OpenAiService.kt +++ b/app/src/main/java/com/mohandass/botforge/common/services/OpenAiService.kt @@ -26,4 +26,11 @@ interface OpenAiService { n: Int = 1, imageSize: ImageSize = ImageSize.is256x256, ): List + + @OptIn(BetaOpenAI::class) + suspend fun generateImageVariant( + original: ByteArray, + n: Int, + imageSize: ImageSize, + ): List } diff --git a/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt b/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt index 2e12337..2db4806 100644 --- a/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt +++ b/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt @@ -7,9 +7,11 @@ package com.mohandass.botforge.common.services.implementation import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.chat.ChatCompletion import com.aallam.openai.api.chat.ChatCompletionRequest +import com.aallam.openai.api.file.FileSource import com.aallam.openai.api.image.ImageCreation import com.aallam.openai.api.image.ImageSize import com.aallam.openai.api.image.ImageURL +import com.aallam.openai.api.image.ImageVariation import com.aallam.openai.api.model.ModelId import com.aallam.openai.client.OpenAI import com.mohandass.botforge.chat.model.Message @@ -18,6 +20,8 @@ import com.mohandass.botforge.chat.model.Role import com.mohandass.botforge.common.services.Logger import com.mohandass.botforge.common.services.OpenAiService import com.mohandass.botforge.settings.service.SharedPreferencesService +import okio.Source +import okio.source /** * An implementation of the OpenAiService interface @@ -124,6 +128,48 @@ class OpenAiServiceImpl private constructor( } } + @OptIn(BetaOpenAI::class) + override suspend fun generateImageVariant( + original: ByteArray, + n: Int, + imageSize: ImageSize, + ): List { + logger.logVerbose(TAG, "generateImageVariant()") + try { + val source: Source = original.inputStream().source() + + val fileSource = FileSource(name = "original.png", source = source) + val images = getClient().imageURL( // or openAI.imageJSON + variation = ImageVariation( + image = fileSource, + n = n, + size = imageSize + ) + ) + + // Update usage image count + when (imageSize) { + ImageSize.is256x256 -> { + sharedPreferencesService.incrementUsageImageSmallCount(n) + } + + ImageSize.is512x512 -> { + sharedPreferencesService.incrementUsageImageMediumCount(n) + } + + ImageSize.is1024x1024 -> { + sharedPreferencesService.incrementUsageImageLargeCount(n) + } + } + + logger.logVerbose(TAG, "generateImageVariant() $images") + return images + } catch (e: Exception) { + logger.logError(TAG, "generateImageVariant() ${e.printStackTrace()}", e) + throw e + } + } + companion object { private const val TAG = "OpenAiService" diff --git a/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt b/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt index 1355c7e..29183f5 100644 --- a/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt +++ b/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt @@ -286,6 +286,17 @@ fun ImageUi( } } + IconButton( + onClick = { + imageViewModel.generateImageVariant() + } + ) { + Icon( + painter = painterResource(id = R.drawable.baseline_refresh_24), + contentDescription = stringResource(id = R.string.generate_variant), + ) + } + IconButton( onClick = { imageViewModel.shareImage(context) diff --git a/app/src/main/java/com/mohandass/botforge/image/viewmodel/ImageViewModel.kt b/app/src/main/java/com/mohandass/botforge/image/viewmodel/ImageViewModel.kt index e421422..ff4b451 100644 --- a/app/src/main/java/com/mohandass/botforge/image/viewmodel/ImageViewModel.kt +++ b/app/src/main/java/com/mohandass/botforge/image/viewmodel/ImageViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream import java.net.URL import java.util.Date import javax.inject.Inject @@ -254,6 +255,73 @@ class ImageViewModel @Inject constructor( analytics.logImageExported() } + @OptIn(BetaOpenAI::class, ExperimentalFoundationApi::class) + fun generateImageVariant() { + val originalBitmap = imageUriList[pagerState.value.currentPage] as Bitmap + val stream = ByteArrayOutputStream() + originalBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + + showImage.value = false + setLoading(true) + + job = viewModelScope.launch { + try { + val images = openAiService.generateImageVariant( + original = stream.toByteArray(), + imageSize = imageSize.value, + n = n.value + ) + + imageUriList.clear() + imageUriList.addAll(images.map { it.url }) + + saveGeneratedImages { + setLoading(false) + analytics.logImageGenerated() + } + + } catch (e: Exception) { + logger.logError(TAG, "generateImageVariant() error: $e", e) + if (e.message != null) { + logger.logError(TAG, "generateImageVariant() error m: ${e.message}", e) + SnackbarManager.showMessage( + e.toSnackbarMessageWithAction(R.string.settings) { + appState.navControllerMain.navigate(AppRoutes.MainRoutes.ApiKeySettings.route) + }) + } else { + logger.logError(TAG, "generateImageVariant() error st: ${e.stackTrace}", e) + + setLoading(false) + + val message = Utils.parseStackTraceForErrorMessage(e) + + // Attempt to parse the error message + when (message.message) { + Utils.INVALID_API_KEY_ERROR_MESSAGE -> { + SnackbarManager.showMessageWithAction( + R.string.invalid_api_key, + R.string.settings + ) { + appState.navControllerMain.navigate(AppRoutes.MainRoutes.ApiKeySettings.route) + } + } + + Utils.INTERRUPTED_ERROR_MESSAGE -> { + SnackbarManager.showMessage(R.string.request_cancelled) + } + + else -> { + SnackbarManager.showMessage( + message.toSnackbarMessageWithAction(R.string.settings) { + appState.navControllerMain.navigate(AppRoutes.MainRoutes.Settings.route) + }) + } + } + } + } + } + } + @OptIn(BetaOpenAI::class) fun generateImageFromPrompt() { if (prompt.value.isBlank()) { @@ -281,15 +349,15 @@ class ImageViewModel @Inject constructor( } } catch (e: Exception) { - logger.logError(TAG, "getChatCompletion() error: $e", e) + logger.logError(TAG, "generateImageFromPrompt() error: $e", e) if (e.message != null) { - logger.logError(TAG, "getChatCompletion() error m: ${e.message}", e) + logger.logError(TAG, "generateImageFromPrompt() error m: ${e.message}", e) SnackbarManager.showMessage( e.toSnackbarMessageWithAction(R.string.settings) { appState.navControllerMain.navigate(AppRoutes.MainRoutes.ApiKeySettings.route) }) } else { - logger.logError(TAG, "getChatCompletion() error st: ${e.stackTrace}", e) + logger.logError(TAG, "generateImageFromPrompt() error st: ${e.stackTrace}", e) setLoading(false) diff --git a/app/src/main/res/drawable/baseline_refresh_24.xml b/app/src/main/res/drawable/baseline_refresh_24.xml new file mode 100644 index 0000000..686a247 --- /dev/null +++ b/app/src/main/res/drawable/baseline_refresh_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fcd4ecc..3b4c01b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -344,6 +344,8 @@ This will delete all generated images stored locally No images yet. Image deleted! + Generate Variant + This will generate a new variant of the image. Account linked! From f5986f6239fa704deee65cb6a2ecf93175604dd3 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Wed, 26 Apr 2023 18:12:10 -0400 Subject: [PATCH 4/7] fix: Shake Sensitivity padding --- .../java/com/mohandass/botforge/settings/ui/SettingsUi.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt index 685f8fe..15441f1 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack @@ -273,6 +274,8 @@ fun SettingsUi( ) { Column { Row { + Spacer(modifier = Modifier.width(70.dp)) + Text( text = resources().getString(R.string.shake_sensitivity), modifier = Modifier.padding(10.dp), @@ -297,7 +300,7 @@ fun SettingsUi( settingsViewModel.setShakeToClearSensitivity(shakeSensitivity) }, valueRange = 0f..Constants.MAX_SENSITIVITY_THRESHOLD, - modifier = Modifier.padding(horizontal = 15.dp) + modifier = Modifier.padding(start = 75.dp, end = 15.dp) ) } } From 9c595f8c30c2c80b57b600bbd426de18afeee074 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Thu, 27 Apr 2023 18:17:09 -0400 Subject: [PATCH 5/7] feat.: Impl. changing API Timeouts --- .../java/com/mohandass/botforge/AppRoutes.kt | 1 + .../mohandass/botforge/common/Constants.kt | 6 +- .../services/implementation/OpenAiService.kt | 12 ++- .../mohandass/botforge/common/ui/MainUi.kt | 51 ++++++++++++ .../mohandass/botforge/image/ui/ImageUi.kt | 2 +- .../image/ui/components/NumberPicker.kt | 6 +- .../service/SharedPreferencesService.kt | 3 + .../SharedPreferencesServiceImpl.kt | 16 ++++ .../botforge/settings/ui/ApiAdvancedUi.kt | 82 +++++++++++++++++++ .../botforge/settings/ui/ApiUsageUi.kt | 21 +++-- .../botforge/settings/ui/SettingsUi.kt | 18 +++- .../settings/ui/components/SettingsItem.kt | 2 +- .../viewmodel/AdvancedApiSettingsViewModel.kt | 31 +++++++ .../res/drawable/baseline_settings_24.xml | 5 ++ app/src/main/res/values/strings.xml | 12 +++ 15 files changed, 248 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/mohandass/botforge/settings/ui/ApiAdvancedUi.kt create mode 100644 app/src/main/java/com/mohandass/botforge/settings/viewmodel/AdvancedApiSettingsViewModel.kt create mode 100644 app/src/main/res/drawable/baseline_settings_24.xml diff --git a/app/src/main/java/com/mohandass/botforge/AppRoutes.kt b/app/src/main/java/com/mohandass/botforge/AppRoutes.kt index a35a1af..2e89ac5 100644 --- a/app/src/main/java/com/mohandass/botforge/AppRoutes.kt +++ b/app/src/main/java/com/mohandass/botforge/AppRoutes.kt @@ -51,6 +51,7 @@ sealed class AppRoutes(val route: String) { object Settings : MainRoutes("main_settings") object ApiKeySettings : MainRoutes("main_settings_api_key") object ApiUsageSettings : MainRoutes("main_settings_api_usage") + object ApiAdvancedSettings : MainRoutes("main_settings_api_advanced") object ManageAccountSettings : MainRoutes("main_settings_manage_account") object OpenSourceLicenses : MainRoutes("main_settings_open_source_licenses") object IconCredits : MainRoutes("main_settings_icon_credits") diff --git a/app/src/main/java/com/mohandass/botforge/common/Constants.kt b/app/src/main/java/com/mohandass/botforge/common/Constants.kt index a51602c..a21289d 100644 --- a/app/src/main/java/com/mohandass/botforge/common/Constants.kt +++ b/app/src/main/java/com/mohandass/botforge/common/Constants.kt @@ -6,6 +6,10 @@ package com.mohandass.botforge.common class Constants { companion object { + const val DEFAULT_API_TIMEOUT = 60 + const val MAX_API_TIMEOUT = 300 + const val MIN_API_TIMEOUT = 15 + const val ANIMATION_DURATION = 400 const val ANIMATION_OFFSET = 400 @@ -16,4 +20,4 @@ class Constants { const val MAX_SENSITIVITY_THRESHOLD = 5f } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt b/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt index 2db4806..6b6cacb 100644 --- a/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt +++ b/app/src/main/java/com/mohandass/botforge/common/services/implementation/OpenAiService.kt @@ -8,12 +8,15 @@ import com.aallam.openai.api.BetaOpenAI import com.aallam.openai.api.chat.ChatCompletion import com.aallam.openai.api.chat.ChatCompletionRequest import com.aallam.openai.api.file.FileSource +import com.aallam.openai.api.http.Timeout import com.aallam.openai.api.image.ImageCreation import com.aallam.openai.api.image.ImageSize import com.aallam.openai.api.image.ImageURL import com.aallam.openai.api.image.ImageVariation +import com.aallam.openai.api.logging.LogLevel import com.aallam.openai.api.model.ModelId import com.aallam.openai.client.OpenAI +import com.aallam.openai.client.OpenAIConfig import com.mohandass.botforge.chat.model.Message import com.mohandass.botforge.chat.model.MessageMetadata import com.mohandass.botforge.chat.model.Role @@ -22,6 +25,7 @@ import com.mohandass.botforge.common.services.OpenAiService import com.mohandass.botforge.settings.service.SharedPreferencesService import okio.Source import okio.source +import kotlin.time.Duration.Companion.seconds /** * An implementation of the OpenAiService interface @@ -42,7 +46,13 @@ class OpenAiServiceImpl private constructor( throw Exception("No API key found") } - return OpenAI(apiKey) + val config = OpenAIConfig( + token = apiKey, + logLevel = LogLevel.Info, + timeout = Timeout(socket = sharedPreferencesService.getApiTimeout().seconds), + ) + + return OpenAI(config) } @OptIn(BetaOpenAI::class) diff --git a/app/src/main/java/com/mohandass/botforge/common/ui/MainUi.kt b/app/src/main/java/com/mohandass/botforge/common/ui/MainUi.kt index 36b879a..9dd39ea 100644 --- a/app/src/main/java/com/mohandass/botforge/common/ui/MainUi.kt +++ b/app/src/main/java/com/mohandass/botforge/common/ui/MainUi.kt @@ -27,6 +27,7 @@ import com.mohandass.botforge.AppState import com.mohandass.botforge.chat.ui.PersonaUi import com.mohandass.botforge.chat.ui.components.header.top.TopBar import com.mohandass.botforge.common.Constants +import com.mohandass.botforge.settings.ui.ApiAdvancedUi import com.mohandass.botforge.settings.ui.ApiKeyUi import com.mohandass.botforge.settings.ui.IconCreditsUi import com.mohandass.botforge.settings.ui.ManageAccountUi @@ -265,6 +266,56 @@ fun MainUi( ) { ApiUsageUi(settingsViewModel = hiltViewModel()) } + composable( + route = AppRoutes.MainRoutes.ApiAdvancedSettings.route, + enterTransition = { + slideInHorizontally( + initialOffsetX = { Constants.ANIMATION_OFFSET }, + animationSpec = tween( + durationMillis = Constants.ANIMATION_DURATION, + easing = FastOutSlowInEasing + ) + ) + fadeIn( + animationSpec = tween(Constants.ANIMATION_DURATION) + ) + + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { -Constants.ANIMATION_OFFSET }, + animationSpec = tween( + durationMillis = Constants.ANIMATION_DURATION, + easing = FastOutSlowInEasing + ) + ) + fadeOut( + animationSpec = tween(Constants.ANIMATION_DURATION) + ) + }, + popEnterTransition = { + slideInHorizontally( + initialOffsetX = { -Constants.ANIMATION_OFFSET }, + animationSpec = tween( + durationMillis = Constants.ANIMATION_DURATION, + easing = FastOutSlowInEasing + ) + ) + fadeIn( + animationSpec = tween(Constants.ANIMATION_DURATION) + ) + }, + popExitTransition = { + slideOutHorizontally( + targetOffsetX = { Constants.ANIMATION_OFFSET }, + animationSpec = tween( + durationMillis = Constants.ANIMATION_DURATION, + easing = FastOutSlowInEasing + ) + ) + fadeOut( + animationSpec = tween(Constants.ANIMATION_DURATION) + ) + } + ) { + ApiAdvancedUi() + } composable( route = AppRoutes.MainRoutes.ManageAccountSettings.route, enterTransition = { diff --git a/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt b/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt index 29183f5..1f605ea 100644 --- a/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt +++ b/app/src/main/java/com/mohandass/botforge/image/ui/ImageUi.kt @@ -416,7 +416,7 @@ fun ImageUi( NumberPicker( modifier = Modifier .weight(1f), - n = n, + numberAsString = n.toString(), onIncrement = { if (n < Constants.MAX_IMAGE_GENERATION_COUNT) { n++ diff --git a/app/src/main/java/com/mohandass/botforge/image/ui/components/NumberPicker.kt b/app/src/main/java/com/mohandass/botforge/image/ui/components/NumberPicker.kt index 62ec387..4a56f4c 100644 --- a/app/src/main/java/com/mohandass/botforge/image/ui/components/NumberPicker.kt +++ b/app/src/main/java/com/mohandass/botforge/image/ui/components/NumberPicker.kt @@ -31,7 +31,7 @@ import com.mohandass.botforge.R @Composable fun NumberPicker( modifier: Modifier = Modifier, - n: Int, + numberAsString: String, onIncrement: () -> Unit, onDecrement: () -> Unit, ) { @@ -69,7 +69,7 @@ fun NumberPicker( disabledContentColor = MaterialTheme.colorScheme.primary ) ) { - Text(text = n.toString()) + Text(text = numberAsString) } OutlinedButton( @@ -98,7 +98,7 @@ fun NumberPickerPreview() { } NumberPicker( - n = n, + numberAsString = n.toString(), onIncrement = { n += 1 }, diff --git a/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt b/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt index 537c31a..447a209 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/service/SharedPreferencesService.kt @@ -12,6 +12,8 @@ interface SharedPreferencesService { // API Key fun getApiKey(): String fun setAPIKey(apiKey: String) + fun getApiTimeout(): Int + fun setApiTimeout(timeout: Int) // OnBoarding fun getOnBoardingCompleted(): Boolean @@ -35,6 +37,7 @@ interface SharedPreferencesService { companion object { const val USER_PREFERENCES_NAME = "user_preferences" const val API_KEY = "open_ai_api_key" + const val API_TIMEOUT = "open_ai_api_timeout" const val API_USAGE_AS_TOKENS = "open_ai_api_usage_tokens" const val API_USAGE_IMAGE_SMALL_COUNT = "open_ai_api_usage_image_small_count" const val API_USAGE_IMAGE_MEDIUM_COUNT = "open_ai_api_usage_image_medium_count" diff --git a/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt b/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt index 0382347..f3578eb 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/service/implementation/SharedPreferencesServiceImpl.kt @@ -6,6 +6,7 @@ package com.mohandass.botforge.settings.service.implementation import android.content.Context import androidx.core.content.edit +import com.mohandass.botforge.common.Constants import com.mohandass.botforge.settings.service.SharedPreferencesService /** @@ -39,6 +40,21 @@ class SharedPreferencesServiceImpl private constructor(context: Context) : } } + private val _timeout: Int + get() { + return sharedPreferences.getInt(SharedPreferencesService.API_TIMEOUT, Constants.DEFAULT_API_TIMEOUT) + } + + override fun getApiTimeout(): Int { + return _timeout + } + + override fun setApiTimeout(timeout: Int) { + sharedPreferences.edit { + putInt(SharedPreferencesService.API_TIMEOUT, timeout) + } + } + private val _usageTokens: Long get() { return sharedPreferences.getLong(SharedPreferencesService.API_USAGE_AS_TOKENS, 0) diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiAdvancedUi.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiAdvancedUi.kt new file mode 100644 index 0000000..d1ee5d1 --- /dev/null +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiAdvancedUi.kt @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Dheshan Mohandass (L4TTiCe) +// +// SPDX-License-Identifier: MIT + +package com.mohandass.botforge.settings.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.mohandass.botforge.R +import com.mohandass.botforge.common.Constants +import com.mohandass.botforge.image.ui.components.NumberPicker +import com.mohandass.botforge.settings.viewmodel.AdvancedApiSettingsViewModel + +@Composable +fun ApiAdvancedUi( + advancedApiSettingsViewModel: AdvancedApiSettingsViewModel = hiltViewModel(), +) { + var apiTimeout by remember { advancedApiSettingsViewModel.apiTimeout } + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(10.dp) + ) { + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = stringResource(id = R.string.advanced_api_settings), + modifier = Modifier.padding(horizontal = 10.dp), + style = MaterialTheme.typography.titleLarge, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = stringResource(id = R.string.api_timeout), + modifier = Modifier.padding(10.dp), + style = MaterialTheme.typography.titleMedium + ) + + Text( + text = stringResource(id = R.string.api_timeout_description), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + NumberPicker( + numberAsString = "$apiTimeout s", + onIncrement = { + if (apiTimeout < Constants.MAX_API_TIMEOUT) { + apiTimeout++ + advancedApiSettingsViewModel.setApiTimeout() + } + }, + onDecrement = { + if (apiTimeout > Constants.MIN_API_TIMEOUT) { + apiTimeout-- + advancedApiSettingsViewModel.setApiTimeout() + } + }, + ) + } +} diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt index 270e83f..327fe09 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/ApiUsageUi.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Icon @@ -63,6 +65,7 @@ fun ApiUsageUi( Column( modifier = Modifier + .verticalScroll(rememberScrollState()) .fillMaxSize() .padding(10.dp) ) { @@ -70,7 +73,7 @@ fun ApiUsageUi( Spacer(modifier = Modifier.height(10.dp)) Text( - text = resources().getString(R.string.api_usage), + text = stringResource(R.string.api_usage), modifier = Modifier.padding(horizontal = 10.dp), style = MaterialTheme.typography.titleLarge, ) @@ -78,13 +81,13 @@ fun ApiUsageUi( Spacer(modifier = Modifier.height(10.dp)) Text( - text = resources().getString(R.string.usage_quotas), + text = stringResource(R.string.usage_quotas), modifier = Modifier.padding(10.dp), style = MaterialTheme.typography.titleMedium ) Text( - text = resources().getString(R.string.usage_quotas_message), + text = stringResource(R.string.usage_quotas_message), modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), style = MaterialTheme.typography.bodyMedium, ) @@ -95,7 +98,7 @@ fun ApiUsageUi( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = resources().getString(R.string.your_usage), + text = stringResource(R.string.your_usage), modifier = Modifier.padding(start = 10.dp), style = MaterialTheme.typography.titleMedium ) @@ -311,7 +314,7 @@ fun ApiUsageUi( } ) { Text( - text = resources().getString(R.string.reset_usage), + text = stringResource(R.string.reset_usage), color = MaterialTheme.colorScheme.error, ) } @@ -322,8 +325,8 @@ fun ApiUsageUi( SettingsCategory(title = stringResource(id = R.string.external_links)) SettingsItem( - title = resources().getString(R.string.usage_summary), - description = resources().getString(R.string.usage_summary_message), + title = stringResource(R.string.usage_summary), + description = stringResource(R.string.usage_summary_message), painter = painterResource(id = R.drawable.baseline_data_usage_24), ) { val intent = Intent(Intent.ACTION_VIEW).apply { @@ -333,8 +336,8 @@ fun ApiUsageUi( } SettingsItem( - title = resources().getString(R.string.open_ai_pricing), - description = resources().getString(R.string.open_ai_pricing_message), + title = stringResource(R.string.open_ai_pricing), + description = stringResource(R.string.open_ai_pricing_message), painter = painterResource(id = R.drawable.baseline_price_check_24), ) { val intent = Intent(Intent.ACTION_VIEW).apply { diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt index 15441f1..c7db988 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/SettingsUi.kt @@ -190,10 +190,20 @@ fun SettingsUi( }) ) } + item { + SettingsItem( + title = stringResource(id = R.string.advanced_api_settings), + description = stringResource(id = R.string.advanced_api_settings_message), + painter = painterResource(id = R.drawable.baseline_settings_24), + onClick = ({ + appViewModel.appState.navControllerMain.navigate(AppRoutes.MainRoutes.ApiAdvancedSettings.route) + }) + ) + } item{ SettingsItem( - title = "Enable Image Generation", - description = "Allows generating Images using the API", + title = stringResource(id = R.string.enable_image_generation), + description = stringResource(id = R.string.enable_image_generation_message), icon = painterResource(id = R.drawable.picture), switchState = isImageGenerationEnabled, onCheckChange = { @@ -242,8 +252,8 @@ fun SettingsUi( } item { SettingsItem( - title = "Auto-Generate Chat Title", - description = "Automatically generates Title when saving Chats, uses OpenAI API", + title = stringResource(id = R.string.auto_generate_chat_title), + description = stringResource(id = R.string.auto_generate_chat_title_message), icon = painterResource(id = R.drawable.baseline_title_24), switchState = isAutoChatNameEnabled, onCheckChange = { diff --git a/app/src/main/java/com/mohandass/botforge/settings/ui/components/SettingsItem.kt b/app/src/main/java/com/mohandass/botforge/settings/ui/components/SettingsItem.kt index 262abe9..5d6cc71 100644 --- a/app/src/main/java/com/mohandass/botforge/settings/ui/components/SettingsItem.kt +++ b/app/src/main/java/com/mohandass/botforge/settings/ui/components/SettingsItem.kt @@ -129,7 +129,7 @@ fun SettingsItemPreview() { ) } -@Preview +@Preview(showBackground = true) @Composable fun SettingsItemSwitchPreview() { SettingsItem( diff --git a/app/src/main/java/com/mohandass/botforge/settings/viewmodel/AdvancedApiSettingsViewModel.kt b/app/src/main/java/com/mohandass/botforge/settings/viewmodel/AdvancedApiSettingsViewModel.kt new file mode 100644 index 0000000..cbdaf69 --- /dev/null +++ b/app/src/main/java/com/mohandass/botforge/settings/viewmodel/AdvancedApiSettingsViewModel.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Dheshan Mohandass (L4TTiCe) +// +// SPDX-License-Identifier: MIT + +package com.mohandass.botforge.settings.viewmodel + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import com.mohandass.botforge.common.services.Analytics +import com.mohandass.botforge.common.services.Logger +import com.mohandass.botforge.settings.service.SharedPreferencesService +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class AdvancedApiSettingsViewModel @Inject constructor( + private val sharedPreferencesService: SharedPreferencesService, + private val logger: Logger, + private val analytics: Analytics, +) : ViewModel() { + val apiTimeout = mutableStateOf(sharedPreferencesService.getApiTimeout()) + + fun setApiTimeout() { + logger.log(TAG, "setApiTimeout()") + sharedPreferencesService.setApiTimeout(apiTimeout.value) + } + + companion object { + private const val TAG = "AdvancedApiSettingsViewModel" + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml new file mode 100644 index 0000000..c44b142 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b4c01b..a8f5fa5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,10 +162,22 @@ API View or change your API Key. + Enable Image Generation + Allows generating Images using the API Usage Tokens Dollars Check your API usage information, and estimated cost. + Advanced API Settings + Manage Timeouts to accommodate longer responses to prompts + API Timeout + Timeout is the time in seconds that the app will wait for + a response from API before giving up. If your prompts require longer to generate, you can + try increasing this value. + + + Auto-Generate Chat Title + Automatically generates Title when saving Chats, uses OpenAI API Manage Account View or change your account information. From fb9409520835f3ef96838c2063b879142fe3ada9 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Thu, 27 Apr 2023 18:41:49 -0400 Subject: [PATCH 6/7] fix: PersonaList delete not visible --- .../botforge/chat/ui/PersonaListUi.kt | 3 +-- .../chat/viewmodel/PersonaListViewModel.kt | 22 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/mohandass/botforge/chat/ui/PersonaListUi.kt b/app/src/main/java/com/mohandass/botforge/chat/ui/PersonaListUi.kt index f0a4132..e3ce857 100644 --- a/app/src/main/java/com/mohandass/botforge/chat/ui/PersonaListUi.kt +++ b/app/src/main/java/com/mohandass/botforge/chat/ui/PersonaListUi.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -46,7 +45,7 @@ fun PersonaListUi( personaViewModel: PersonaViewModel = hiltViewModel(), browseViewModel: BrowseViewModel = hiltViewModel(), ) { - val personas by personaViewModel.personas.observeAsState(initial = emptyList()) + val personas = personaListViewModel.personas val matchedPersonas = personaListViewModel.matchedPersonas var showDeleteAllPersonaDialog by personaListViewModel.showDeleteAllPersonaDialog diff --git a/app/src/main/java/com/mohandass/botforge/chat/viewmodel/PersonaListViewModel.kt b/app/src/main/java/com/mohandass/botforge/chat/viewmodel/PersonaListViewModel.kt index 076c873..8a836d3 100644 --- a/app/src/main/java/com/mohandass/botforge/chat/viewmodel/PersonaListViewModel.kt +++ b/app/src/main/java/com/mohandass/botforge/chat/viewmodel/PersonaListViewModel.kt @@ -33,22 +33,22 @@ class PersonaListViewModel @Inject constructor( private val botService: BotService, private val logger: Logger ) : ViewModel() { - private var _personas = mutableStateListOf() - val personas = personaRepository.personas.asLiveData() + var personas = mutableStateListOf() + private val _personas = personaRepository.personas.asLiveData() // Reference: // https://stackoverflow.com/questions/48396092/should-i-include-lifecycleowner-in-viewmodel private val observer: (List) -> Unit = { - _personas.clear() - _personas.addAll(it) + personas.clear() + personas.addAll(it) } init { - personas.observeForever(observer) + _personas.observeForever(observer) } override fun onCleared() { - personas.removeObserver(observer) + _personas.removeObserver(observer) super.onCleared() } @@ -72,7 +72,7 @@ class PersonaListViewModel @Inject constructor( if (searchQuery.value.isEmpty() || searchQuery.value.isBlank()) { return } - personas.value!!.filterTo(matchedPersonas) { persona -> + _personas.value!!.filterTo(matchedPersonas) { persona -> persona.name.contains(searchQuery.value, ignoreCase = true) || persona.alias.contains(searchQuery.value, ignoreCase = true) || @@ -99,16 +99,16 @@ class PersonaListViewModel @Inject constructor( fun deletePersona(uuid: String) { var deleteJob: Job = Job() - val persona = _personas.find { it.uuid == uuid } + val persona = personas.find { it.uuid == uuid } if (persona != null) { // Remove persona from the list - _personas.remove(persona) + personas.remove(persona) SnackbarManager.showMessageWithAction( R.string.deleted_persona, R.string.undo ) { - _personas.add(persona) + personas.add(persona) deleteJob.cancel() } @@ -124,7 +124,7 @@ class PersonaListViewModel @Inject constructor( fun fetchBots() { logger.logVerbose(TAG, "fetchBots") - for (persona in personas.value!!) { + for (persona in _personas.value!!) { viewModelScope.launch { bots[persona.parentUuid] = botService.getBot(persona.parentUuid) logger.logVerbose( From 4c53d51e148cb76f19607fe97544db250e170a23 Mon Sep 17 00:00:00 2001 From: Dheshan Mohandass Date: Thu, 27 Apr 2023 18:43:38 -0400 Subject: [PATCH 7/7] Release: Build #31 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bd3095d..dd8514e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,8 +37,8 @@ android { applicationId = "com.mohandass.botforge" minSdk = 28 targetSdk = 33 - versionCode = 30 - versionName = "1.3.0" + versionCode = 31 + versionName = "1.3.1" vectorDrawables.useSupportLibrary = true signingConfig = signingConfigs.getByName("debug")