package com.example.aiapp import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle /** * A line across the transcript saying what left the session's context. * * Centred between two rules, because it is a divider rather than something anybody said. Two things * produce one -- a compaction and a clear -- and they are drawn the same way on purpose: to a * reader scrolling back, both mean "the session no longer has what is above this", and which of the * two it was is said by the words and the colour. * * The rules take [color] too, so the whole divider reads as one mark of one kind. */ @Composable fun TranscriptDivider(text: String, color: Color, modifier: Modifier = Modifier) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier.fillMaxWidth().padding(vertical = 8.dp), ) { HorizontalDivider(Modifier.weight(1f), color = color) Text(text, style = MaterialTheme.typography.bodySmall, color = color) HorizontalDivider(Modifier.weight(1f), color = color) } } /** * The mark a clear leaves. * * Red, and no counts: a clear takes the conversation out of what the session is given, and unlike a * compaction it summarises nothing and measures nothing. Everything above stays on screen and stays * scrollable -- the reader can see that, which is why this does not say it. */ @Composable fun ClearedRow(modifier: Modifier = Modifier) { TranscriptDivider("Context cleared", clearedColor, modifier) } /** * The mark running out of quota leaves. * * The same red the usage bar takes when a window is spent, because it is the same fact in a second * place: colour by consequence, so "there is nothing left to spend" is learned once. * * A time rather than a countdown. The row is folded once and never re-measured, so a span would go * stale on screen the moment it was drawn; and this is when the *account* said it would reset, * which is not a promise about when the session picks back up. A limit the session was told no * reset time for says nothing about one -- that state has its own words rather than a plausible * number. */ @Composable fun LimitRow(item: TranscriptItem.LimitNote, modifier: Modifier = Modifier) { TranscriptDivider(limitSummary(item.resetsAt, ZoneId.systemDefault()), overLimitColor, modifier) } /** * What the row says. Split out so the wording is testable without a screen, since the two states it * has to keep apart -- a reset time that arrived and one that never did -- are exactly the pair * that reads the same when it goes wrong. * * [zone] is a parameter rather than read here so a test says the same thing wherever it runs. */ fun limitSummary(resetsAt: Double?, zone: ZoneId): String { val at = resetsAt?.let { try { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) .withZone(zone) .format(Instant.ofEpochSecond(it.toLong())) } catch (_: Exception) { null } } return if (at == null) "Usage limit reached" else "Usage limit reached • resets $at" }