Answer a question card as one act, and mark the press that made it
Picking an option marked nothing until the answer had crossed the tunnel, been recorded and come back as an event, so the card sat unchanged for most of a second after a tap. What the reader has picked is now the card's own state and shows at once; a Submit at the foot sends every question the tool is waiting on, greyed until all of them have an answer and a spinner while the request is out. Several questions are paged rather than stacked, with the count and a pair of arrows on the right, because three questions with four described options each is several screens and the reader scrolls past the one they are answering to reach the button that sends it. A permission ask keeps its single tap -- two bare words are not worth a submit step -- and marks what was pressed until the request settles, so the mark either stands on the recorded answer or goes away with the failure. Chevron draws all four directions from one description of the shape, since the pager needed two more of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4d512af26
commit
8dcd2cb708
7 files changed
+299
-127
No files matched your search
@@ -19,7 +19,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -174,7 +177,7 @@ fun ToolGroup(
|
||||
onToggle: () -> Unit,
|
||||
isToolExpanded: (String) -> Boolean,
|
||||
onToolToggle: (String) -> Unit,
|
||||
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
||||
onAnswer: (List<QuestionAnswer>, onSettled: () -> Unit) -> Unit,
|
||||
image: @Composable (String) -> Unit,
|
||||
) {
|
||||
val heading = "Called ${group.calls.size} tools"
|
||||
@@ -254,7 +257,7 @@ private fun CollapseBar(height: Dp, onToggle: () -> Unit) {
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Chevron(pointingUp = true, colour = colour)
|
||||
Chevron(Pointing.Up, colour = colour)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +309,7 @@ fun ToolCard(
|
||||
tool: TranscriptItem.ToolRun,
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
||||
onAnswer: (List<QuestionAnswer>, onSettled: () -> Unit) -> Unit,
|
||||
image: @Composable (String) -> Unit = {},
|
||||
/** Square where this card faces another in a group; see [connectedShape]. */
|
||||
shape: Shape = CardDefaults.shape,
|
||||
@@ -398,9 +401,7 @@ fun ToolCard(
|
||||
if (tool.tool == ASK_USER_QUESTION) {
|
||||
AskUserQuestionBody(tool.asks, onAnswer)
|
||||
} else {
|
||||
tool.asks.forEach { ask ->
|
||||
PermissionAsk(ask) { answers -> onAnswer(ask.id, answers) }
|
||||
}
|
||||
tool.asks.forEach { ask -> PermissionAsk(ask, onAnswer) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -414,7 +415,17 @@ fun ToolCard(
|
||||
* the ask can stand alone, and here it does not have to -- the card above is showing exactly that.
|
||||
*/
|
||||
@Composable
|
||||
private fun PermissionAsk(ask: TranscriptItem.QuestionCard, onAnswer: (List<String>) -> Unit) {
|
||||
private fun PermissionAsk(
|
||||
ask: TranscriptItem.QuestionCard,
|
||||
onAnswer: (List<QuestionAnswer>, onSettled: () -> Unit) -> Unit,
|
||||
) {
|
||||
// What was pressed, before the answer has been round-tripped. Two bare words with no submit
|
||||
// step -- unlike a question card, where the answer is several choices and worth reviewing --
|
||||
// so the press has to be its own acknowledgement or the row sits unchanged for a round trip
|
||||
// and reads as having missed the tap. Cleared when the request settles: by then either the
|
||||
// answer is in `ask.answers` and the mark stands on a measurement, or it failed and the
|
||||
// buttons come back rather than leaving a decision marked that nothing recorded.
|
||||
var pressed by remember(ask.id) { mutableStateOf<String?>(null) }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
ask.prompt.substringBefore('\n'),
|
||||
@@ -425,7 +436,18 @@ private fun PermissionAsk(ask: TranscriptItem.QuestionCard, onAnswer: (List<Stri
|
||||
// [AskedQuestion], which is the same rule on the question card. A permission is where it
|
||||
// matters most: "Answered: Deny" alone does not say that Allow was the alternative, and
|
||||
// whether a tool was allowed or refused is the thing a reader comes back to this row for.
|
||||
AnswerOptions(ask.options, ask.answers, onAnswer.takeIf { ask.answers.isEmpty() })
|
||||
val settled = ask.answers.isNotEmpty()
|
||||
AnswerOptions(
|
||||
ask.options,
|
||||
if (settled) ask.answers else listOfNotNull(pressed),
|
||||
onPick =
|
||||
if (settled || pressed != null) null
|
||||
else
|
||||
{ label ->
|
||||
pressed = label
|
||||
onAnswer(listOf(QuestionAnswer(ask.id, listOf(label)))) { pressed = null }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user