package com.example.aiapp import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp /** * The controls for whatever settings a provider says it takes. * * One composable for both screens that offer them — the spawn form and the session settings dialog * — and for every provider, because the server declares the list (see `DriverKind::params`) rather * than this file knowing it. A driver that grows a setting gets a control here with no change to * the app, which is the whole point: the values that suit one machine ship as defaults, and every * one of them stays reachable from a phone. * * [values] is the whole map and [onChange] hands back the whole map. A key absent from it means the * setting is unset, which is what every [ParamSpec.unset] describes — so clearing a field and never * touching it are deliberately the same state. */ @Composable fun ProviderParamFields( specs: List, values: Map, onChange: (Map) -> Unit, /** * Whether to say which settings wait for a restart. False on a spawn form, where nothing is * running yet and every setting is about to be read — saying it there would be a warning about * a state the reader cannot be in. */ warnAboutRestart: Boolean, modifier: Modifier = Modifier, ) { if (specs.isEmpty()) return Column(modifier.fillMaxWidth()) { specs.forEach { spec -> val set = { value: String -> onChange( // Blank clears rather than storing an empty string: the server reads an absent // key as "use the default", and an empty one would be a value it then failed // to parse. if (value.isBlank()) values - spec.key else values + (spec.key to value) ) } when (spec.kind) { "choice" -> { // The first option is what unset means, so selecting it clears the key — see // `ParamKind::Choice`. Without that the picker could show a default it could // not return to. val default = spec.options.firstOrNull().orEmpty() ChipGroup( label = spec.label + restartSuffix(spec, warnAboutRestart), options = spec.options, selected = values[spec.key] ?: default, onSelect = { chosen -> set(if (chosen == default) "" else chosen) }, ) } else -> OutlinedTextField( value = values[spec.key].orEmpty(), onValueChange = set, label = { Text(spec.label + restartSuffix(spec, warnAboutRestart)) }, placeholder = { Text(spec.unset) }, singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = keyboardFor(spec.kind)), modifier = Modifier.fillMaxWidth(), ) } Spacer(Modifier.height(16.dp)) } if (warnAboutRestart && specs.any { it.restart }) { Text( "A setting marked “on restart” is saved now and read when this session's process " + "next starts.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } /** * Marks a control whose value will not take effect yet. * * On the label rather than beside it, because the reader decides whether to change the thing before * they touch it — a note underneath is read after the decision. */ private fun restartSuffix(spec: ParamSpec, warn: Boolean): String = if (warn && spec.restart) " (on restart)" else "" /** * The keyboard for a value's shape. A number field that opens the letter keyboard is one every * entry is made harder by, and these are nearly all numbers. */ private fun keyboardFor(kind: String): KeyboardType = when (kind) { "integer" -> KeyboardType.Number "decimal" -> KeyboardType.Decimal else -> KeyboardType.Text } /** * How long typing has to stop before edited settings are sent. * * Long enough that a number is one request rather than one per digit, short enough that closing the * dialog straight after typing still saves — the save runs on the screen behind it, which outlives * the dialog, so this delay is not a window the value can be lost in. */ const val PARAM_SAVE_DELAY_MS = 700L