package com.example.aiapp import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth 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.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import java.time.Duration import java.time.OffsetDateTime import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext private val WARN_COLOR = Color(0xFFB26A00) private val OVER_COLOR = Color(0xFFB3261E) /** Window bars for the account's rate limits, with reset times. */ @Composable fun UsageScreen(settings: ServerSettings, onBack: () -> Unit) { val scope = rememberCoroutineScope() var state by remember { mutableStateOf>>(LoadState.Loading) } fun refresh() { state = LoadState.Loading scope.launch { state = try { withContext(Dispatchers.IO) { LoadState.Loaded(fetchUsage(settings)) } } catch (e: ApiException) { LoadState.failed(e) } } } LaunchedEffect(Unit) { refresh() } Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Text( "Usage", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.weight(1f), ) TextButton(onClick = onBack) { Text("Back") } TextButton(onClick = { refresh() }) { Text("Refresh") } } Spacer(Modifier.height(16.dp)) when (val current = state) { is LoadState.Loading -> CircularProgressIndicator() is LoadState.Error -> Text(current.message, color = MaterialTheme.colorScheme.error) is LoadState.Loaded -> current.value.forEach { snapshot -> Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)) { Text(snapshot.provider, style = MaterialTheme.typography.titleMedium) Spacer(Modifier.height(8.dp)) if (!snapshot.available) { Text( snapshot.error ?: "Unavailable", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium, ) } snapshot.windows.forEach { window -> WindowBar(window) Spacer(Modifier.height(12.dp)) } } } Spacer(Modifier.height(12.dp)) } } } } @Composable private fun WindowBar(window: UsageWindow) { val color = when { window.percent >= 95 -> OVER_COLOR window.percent >= 75 -> WARN_COLOR else -> MaterialTheme.colorScheme.primary } Column { Row(modifier = Modifier.fillMaxWidth()) { Text( window.label + if (window.active) " (active)" else "", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f), ) Text("${window.percent.toInt()}%", style = MaterialTheme.typography.bodyMedium) } Spacer(Modifier.height(4.dp)) LinearProgressIndicator( progress = { (window.percent / 100.0).toFloat().coerceIn(0f, 1f) }, color = color, modifier = Modifier.fillMaxWidth(), ) window.resetsAt?.let { Spacer(Modifier.height(2.dp)) Text( "resets ${formatReset(it)}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } /** "in 3h 12m" -- close enough for deciding whether to start a big task. */ private fun formatReset(resetsAt: String): String = try { val until = Duration.between(OffsetDateTime.now(), OffsetDateTime.parse(resetsAt)) when { until.isNegative -> "soon" until.toHours() >= 24 -> "in ${until.toDays()}d ${until.toHours() % 24}h" until.toHours() > 0 -> "in ${until.toHours()}h ${until.toMinutes() % 60}m" else -> "in ${until.toMinutes()}m" } } catch (_: Exception) { "at $resetsAt" }