Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/src/main/java/com/beradeep/aiyo/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import android.content.Context
import com.beradeep.aiyo.data.local.db.dao.ConversationDao
import com.beradeep.aiyo.data.local.db.dao.MessageDao
import com.beradeep.aiyo.data.remote.DataApiClient
import com.beradeep.aiyo.data.repository.AccountRepositoryImpl
import com.beradeep.aiyo.data.repository.ApiKeyRepositoryImpl
import com.beradeep.aiyo.data.repository.ChatRepositoryImpl
import com.beradeep.aiyo.data.repository.ModelRepositoryImpl
import com.beradeep.aiyo.data.repository.SettingRepositoryImpl
import com.beradeep.aiyo.domain.repository.AccountRepository
import com.beradeep.aiyo.domain.repository.ApiKeyRepository
import com.beradeep.aiyo.domain.repository.ChatRepository
import com.beradeep.aiyo.domain.repository.ModelRepository
Expand Down Expand Up @@ -48,4 +50,10 @@ object RepositoryModule {
fun provideSettingsRepository(
@ApplicationContext context: Context
): SettingRepository = SettingRepositoryImpl(context)

@Provides
@Singleton
fun provideAccountRepository(
apiKeyRepository: ApiKeyRepository
): AccountRepository = AccountRepositoryImpl(apiKeyRepository)
}
5 changes: 4 additions & 1 deletion app/src/main/java/com/beradeep/aiyo/di/VmModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.beradeep.aiyo.di

import com.beradeep.aiyo.domain.ApiClient
import com.beradeep.aiyo.domain.repository.AccountRepository
import com.beradeep.aiyo.domain.repository.ApiKeyRepository
import com.beradeep.aiyo.domain.repository.ChatRepository
import com.beradeep.aiyo.domain.repository.ModelRepository
Expand Down Expand Up @@ -34,10 +35,12 @@ constructor(
apiKeyRepository: ApiKeyRepository,
apiClient: ApiClient,
modelRepository: ModelRepository,
settingRepository: SettingRepository
settingRepository: SettingRepository,
accountRepository: AccountRepository
) : SettingsViewModel(
apiKeyRepository = apiKeyRepository,
modelRepository = modelRepository,
settingsRepository = settingRepository,
accountRepository = accountRepository,
apiClient = apiClient
)
1 change: 1 addition & 0 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dependencies {
// Api Client
implementation(platform(libs.openai.client.bom))
implementation(libs.openai.client)
implementation(libs.ktor.client.core)
runtimeOnly(libs.ktor.client.okhttp)

// Room
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.beradeep.aiyo.data.remote

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class OpenRouterCreditsResponse(
val data: OpenRouterCreditsDto
)

@Serializable
data class OpenRouterCreditsDto(
@SerialName("total_credits") val totalCredits: Double = 0.0,
@SerialName("total_usage") val totalUsage: Double = 0.0
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.beradeep.aiyo.data.repository

import com.beradeep.aiyo.data.remote.DataApiClient
import com.beradeep.aiyo.data.remote.OpenRouterCreditsResponse
import com.beradeep.aiyo.domain.model.Credits
import com.beradeep.aiyo.domain.repository.AccountRepository
import com.beradeep.aiyo.domain.repository.ApiKeyRepository
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpHeaders
import kotlinx.serialization.json.Json

class AccountRepositoryImpl(
private val apiKeyRepository: ApiKeyRepository
) : AccountRepository {
private val json = Json { ignoreUnknownKeys = true }
private val httpClient by lazy { HttpClient() }

override suspend fun getCredits(): Result<Credits> = runCatching {
val apiKey = apiKeyRepository.getApiKey()
check(!apiKey.isNullOrBlank()) { "API key is not set" }
val body = httpClient.get(CREDITS_URL) {
header(HttpHeaders.Authorization, "Bearer $apiKey")
}.bodyAsText()
val credits = json.decodeFromString<OpenRouterCreditsResponse>(body).data
Credits(
totalCredits = credits.totalCredits,
totalUsage = credits.totalUsage
)
}

companion object {
private const val CREDITS_URL = DataApiClient.BASE_URL + "credits"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.beradeep.aiyo.domain.model

data class Credits(
val totalCredits: Double,
val totalUsage: Double
) {
val remaining: Double
get() = totalCredits - totalUsage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.beradeep.aiyo.domain.repository

import com.beradeep.aiyo.domain.model.Credits

interface AccountRepository {

suspend fun getCredits(): Result<Credits>
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
ktor-client-core = { module = "io.ktor:ktor-client-core" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
lumo-composables = { group = "com.nomanr", name = "composables", version.ref = "lumo-composables" }
mikepenz-multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRendererAndroid" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.InvertColors
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.ModelTraining
import androidx.compose.material.icons.filled.FormatSize
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.MaterialTheme
Expand All @@ -43,6 +44,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.beradeep.aiyo.domain.model.Credits
import com.beradeep.aiyo.domain.model.Model
import com.beradeep.aiyo.domain.model.ThemeType
import com.beradeep.aiyo.domain.repository.SettingRepository
Expand All @@ -62,6 +64,7 @@ import com.beradeep.aiyo.ui.basics.components.textfield.OutlinedTextField
import com.beradeep.aiyo.ui.basics.components.topbar.TopBar
import com.beradeep.aiyo.ui.screens.chat.components.ModelSelectorChip
import com.beradeep.aiyo.ui.screens.components.ModelSelectionSheet
import java.util.Locale
import kotlin.math.roundToInt

@Composable
Expand Down Expand Up @@ -120,6 +123,14 @@ fun SettingsScreen(
viewModel.onUiEvent(SettingsUiEvent.OnSetApiKey(newKey))
}
)
CreditsSetting(
credits = uiState.credits,
isLoadingCredits = uiState.isLoadingCredits,
hasApiKey = !uiState.apiKey.isNullOrBlank(),
onRefresh = {
viewModel.onUiEvent(SettingsUiEvent.OnRefreshCredits)
}
)
}

HorizontalDivider(
Expand Down Expand Up @@ -335,6 +346,54 @@ private fun ApiKeySetting(
}
}

@Composable
private fun CreditsSetting(
credits: Credits?,
isLoadingCredits: Boolean,
hasApiKey: Boolean,
onRefresh: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Credits",
style = LocalTypography.current.body1
)
Text(
text = when {
!hasApiKey -> "Set an API key to see your balance."
isLoadingCredits -> "Loading..."
credits != null -> String.format(
Locale.US,
"$%.2f remaining ($%.2f used of $%.2f)",
credits.remaining,
credits.totalUsage,
credits.totalCredits
)
else -> "Couldn't load balance. Tap refresh to retry."
},
color = AiyoTheme.colors.textSecondary,
style = AiyoTheme.typography.body3
)
}
if (hasApiKey) {
IconButton(
onClick = onRefresh,
variant = IconButtonVariant.PrimaryGhost
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = "Refresh credits"
)
}
}
}
}

@Composable
private fun ModelSelectionSetting(
selectedModel: Model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ sealed class SettingsUiEvent {
data class OnSetApiKey(val apiKey: String) : SettingsUiEvent()
data class OnModelSelected(val model: com.beradeep.aiyo.domain.model.Model) : SettingsUiEvent()
object OnFetchModels : SettingsUiEvent()
object OnRefreshCredits : SettingsUiEvent()
object OnShowModelSelectionSheet : SettingsUiEvent()
object OnDismissModelSelectionSheet : SettingsUiEvent()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.beradeep.aiyo.ui.screens.settings

import com.beradeep.aiyo.domain.model.Credits
import com.beradeep.aiyo.domain.model.Model
import com.beradeep.aiyo.domain.model.ThemeType
import com.beradeep.aiyo.domain.repository.SettingRepository
Expand All @@ -8,6 +9,8 @@ data class SettingsUiState(
val apiKey: String?,
val models: List<Model>,
val selectedModel: Model,
val credits: Credits?,
val isLoadingCredits: Boolean,
val isFetchingModels: Boolean,
val showModelSelectionSheet: Boolean,
val themeType: ThemeType,
Expand All @@ -20,6 +23,8 @@ data class SettingsUiState(
apiKey = null,
models = listOf(defaultModel),
selectedModel = defaultModel,
credits = null,
isLoadingCredits = false,
isFetchingModels = false,
showModelSelectionSheet = false,
themeType = ThemeType.System,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.beradeep.aiyo.domain.ApiClient
import com.beradeep.aiyo.domain.model.Model
import com.beradeep.aiyo.domain.model.ThemeType
import com.beradeep.aiyo.domain.repository.AccountRepository
import com.beradeep.aiyo.domain.repository.ApiKeyRepository
import com.beradeep.aiyo.domain.repository.ModelRepository
import com.beradeep.aiyo.domain.repository.SettingRepository
Expand All @@ -20,6 +21,7 @@ open class SettingsViewModel(
private val apiKeyRepository: ApiKeyRepository,
private val modelRepository: ModelRepository,
private val settingsRepository: SettingRepository,
private val accountRepository: AccountRepository,
private val apiClient: ApiClient
) : ViewModel() {

Expand All @@ -38,6 +40,7 @@ open class SettingsViewModel(
is SettingsUiEvent.OnSetApiKey -> setApiKey(settingsUiEvent.apiKey)
is SettingsUiEvent.OnModelSelected -> selectModel(settingsUiEvent.model)
SettingsUiEvent.OnFetchModels -> fetchModels()
SettingsUiEvent.OnRefreshCredits -> loadCredits()
SettingsUiEvent.OnShowModelSelectionSheet -> showModelSelectionSheet()
SettingsUiEvent.OnDismissModelSelectionSheet -> dismissModelSelectionSheet()
is SettingsUiEvent.OnUpdateThemeType -> setThemeType(settingsUiEvent.themeType)
Expand All @@ -52,6 +55,15 @@ open class SettingsViewModel(
loadApiKey()
loadDefaultModel()
fetchModels()
loadCredits()
}

private fun loadCredits() {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { it.copy(isLoadingCredits = true) }
val credits = accountRepository.getCredits().getOrNull()
_uiState.update { it.copy(credits = credits, isLoadingCredits = false) }
}
}

private fun loadThemeType() {
Expand Down