Merge branch 'main' of git.arirex.me:iris/ai-app
This commit is contained in:
commit
a4ec8cfbd8
4 files changed
+206
-20
No files matched your search
@@ -671,6 +671,21 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens:
|
|||||||
output; a spinner while `ToolStart` has no matching `ToolEnd`).
|
output; a spinner while `ToolStart` has no matching `ToolEnd`).
|
||||||
- Question cards inline: option buttons for AskUserQuestion, allow/deny for
|
- Question cards inline: option buttons for AskUserQuestion, allow/deny for
|
||||||
permissions, free-text where allowed.
|
permissions, free-text where allowed.
|
||||||
|
- Expanding a row keeps still **the end nearest the tap**: touch a row's
|
||||||
|
upper half and its top edge holds, so it opens downwards; touch its
|
||||||
|
lower half and the bottom edge holds, as the list does by default.
|
||||||
|
Which half, rather than which control, so that everything that opens
|
||||||
|
behaves alike whether or not it has a control at each end — a group's
|
||||||
|
heading and foot bar simply fall in the halves they already occupy. The transcript is laid out from the
|
||||||
|
bottom, so a row's bottom edge is anchored for free and the top one
|
||||||
|
has to be arranged. The correction lives in the *layout* phase
|
||||||
|
(`Modifier.holdTopEdge`): the measurement that discovers the row's new
|
||||||
|
height asks the list to shift by that much, via
|
||||||
|
`requestScrollToItem`, before anything is drawn. Doing it from an
|
||||||
|
effect instead means the wrong position is drawn once first, which
|
||||||
|
reads as a flick and gets worse the faster the screen refreshes
|
||||||
|
(2026-08-30, asked for after groups opened upwards and sent their own
|
||||||
|
heading off the top of the screen).
|
||||||
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
|
- Input bar: text, attach (camera/gallery/file), send — **always enabled**;
|
||||||
mid-run sends become steering messages.
|
mid-run sends become steering messages.
|
||||||
- Top bar: model chip (tap to change), stop button while running, token
|
- Top bar: model chip (tap to change), stop button while running, token
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.example.aiapp
|
package com.example.aiapp
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -32,10 +31,10 @@ import androidx.compose.ui.unit.dp
|
|||||||
fun PeerMessageRow(
|
fun PeerMessageRow(
|
||||||
item: TranscriptItem.PeerNote,
|
item: TranscriptItem.PeerNote,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onToggle: () -> Unit,
|
onToggle: (Float) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Card(modifier.fillMaxWidth().clickable(onClick = onToggle)) {
|
Card(modifier.fillMaxWidth().clickableAt(onToggle)) {
|
||||||
Column(Modifier.padding(12.dp)) {
|
Column(Modifier.padding(12.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Message from ${item.from}", style = MaterialTheme.typography.titleSmall)
|
Text("Message from ${item.from}", style = MaterialTheme.typography.titleSmall)
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
@@ -79,6 +82,67 @@ private const val RECONNECT_DELAY_MS = 1500L
|
|||||||
*/
|
*/
|
||||||
private const val HISTORY_LOOKAHEAD = 8
|
private const val HISTORY_LOOKAHEAD = 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which row was asked to hold its top edge, and how tall it was when it last measured.
|
||||||
|
*
|
||||||
|
* Deliberately *not* snapshot state, and that is the point of the whole class. Both fields are
|
||||||
|
* written from the layout phase; a snapshot write there that composition reads would schedule
|
||||||
|
* another recomposition, and the correction has to land inside the frame that is already being laid
|
||||||
|
* out rather than in a later one. Nothing observes these, so nothing needs to.
|
||||||
|
*
|
||||||
|
* [key] is cleared by the resize it was set for, so it cannot be spent on an unrelated one.
|
||||||
|
*/
|
||||||
|
private class TopEdgeHold {
|
||||||
|
var key: Any? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a row is on screen, so a tap on it can be told which half it landed in.
|
||||||
|
*
|
||||||
|
* Not snapshot state, for the same reason as [TopEdgeHold]: written from layout, read from a click,
|
||||||
|
* and observed by nothing.
|
||||||
|
*/
|
||||||
|
private class RowBounds {
|
||||||
|
var top = 0f
|
||||||
|
var height = 0f
|
||||||
|
|
||||||
|
/** Above this is the row's top half, below it the bottom half. */
|
||||||
|
val middle
|
||||||
|
get() = top + height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One row's height between layouts, so a change in it can be noticed. See [holdTopEdge]. */
|
||||||
|
private class LastHeight {
|
||||||
|
var value: Int? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps this row's top edge where it is when the row changes height, if it was asked to.
|
||||||
|
*
|
||||||
|
* This runs in the *layout* phase, from the measurement that discovers the new height, and that is
|
||||||
|
* the whole reason it is a modifier rather than an effect. A correction posted to a coroutine
|
||||||
|
* arrives a frame or more after the layout it is correcting, so the wrong position is drawn once
|
||||||
|
* before the right one -- visible as a flick, and worse the faster the screen refreshes. Scrolling
|
||||||
|
* from here happens before anything is drawn, so there is no frame to see and nothing that depends
|
||||||
|
* on how quickly the correction is scheduled.
|
||||||
|
*
|
||||||
|
* [hold] is given the change in height. The row's bottom edge is held by the list, so a scroll of
|
||||||
|
* exactly that much is what leaves the top edge where it was.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun Modifier.holdTopEdge(key: Any, held: TopEdgeHold, hold: (Int) -> Unit): Modifier {
|
||||||
|
val last = remember { LastHeight() }
|
||||||
|
return onSizeChanged { size ->
|
||||||
|
val previous = last.value
|
||||||
|
last.value = size.height
|
||||||
|
// A first measurement has no previous height to have moved from, and a row that came
|
||||||
|
// back after being scrolled away is a first measurement again.
|
||||||
|
if (previous == null || previous == size.height || held.key != key) return@onSizeChanged
|
||||||
|
held.key = null
|
||||||
|
hold(size.height - previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the transcript renders: the event stream folded into displayable rows (see [foldEvent]). The
|
* What the transcript renders: the event stream folded into displayable rows (see [foldEvent]). The
|
||||||
* stream is the only data source -- opening this screen replays from seq 0, and a reconnect resumes
|
* stream is the only data source -- opening this screen replays from seq 0, and a reconnect resumes
|
||||||
@@ -477,6 +541,7 @@ fun SessionScreen(
|
|||||||
onSettings: () -> Unit,
|
onSettings: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val topEdgeHeld = remember { TopEdgeHold() }
|
||||||
var items by remember { mutableStateOf(listOf<TranscriptItem>()) }
|
var items by remember { mutableStateOf(listOf<TranscriptItem>()) }
|
||||||
var status by remember { mutableStateOf(summary.status) }
|
var status by remember { mutableStateOf(summary.status) }
|
||||||
// Seeded from the row this screen was opened from, so a conversation already under way says
|
// Seeded from the row this screen was opened from, so a conversation already under way says
|
||||||
@@ -669,6 +734,29 @@ fun SessionScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes a row's height while the end the reader touched stays where it is.
|
||||||
|
*
|
||||||
|
* The transcript is laid out from the bottom, so every row's *bottom* edge is what the list
|
||||||
|
* holds still and all growth goes upward. That is what a tap in a row's lower half already
|
||||||
|
* gets, so it needs nothing: shut a group from the bar at its foot and what follows it does not
|
||||||
|
* move, which is what the reader is looking at down there. A tap in the upper half is the other
|
||||||
|
* case -- left alone it sends the heading under the reader's finger up off the screen and fills
|
||||||
|
* the space above it, so the calls appear on the far side of the control that produced them --
|
||||||
|
* and that one asks the row to hold its top edge instead.
|
||||||
|
*
|
||||||
|
* Which half decides it, rather than which control was pressed, so that everything that opens
|
||||||
|
* behaves the same way whether or not it happens to have a control at each end. A group has two
|
||||||
|
* and its heading and foot bar land in the halves they are already in; a single call is one
|
||||||
|
* card, and tapping low on an open one shuts it downward exactly as the bar does.
|
||||||
|
*
|
||||||
|
* The correction itself belongs to the measurement -- see [holdTopEdge].
|
||||||
|
*/
|
||||||
|
fun toggleAnchored(key: Any, row: RowBounds, at: Float, toggle: () -> Unit) {
|
||||||
|
if (at < row.middle) topEdgeHeld.key = key
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
|
||||||
// A compaction reports nothing about its own progress -- measured against the CLI, which
|
// A compaction reports nothing about its own progress -- measured against the CLI, which
|
||||||
// says it has started, and then says nothing at all until it is done. So what this counts is
|
// says it has started, and then says nothing at all until it is done. So what this counts is
|
||||||
// the one thing anybody here can measure: how long it has been going. A bar filling up would
|
// the one thing anybody here can measure: how long it has been going. A bar filling up would
|
||||||
@@ -1120,25 +1208,54 @@ fun SessionScreen(
|
|||||||
// at the same end. Paging older history is the opposite insertion and was
|
// at the same end. Paging older history is the opposite insertion and was
|
||||||
// already fine, and stays fine, because a key survives both.
|
// already fine, and stays fine, because a key survives both.
|
||||||
items(rows.asReversed(), key = { it.key }) { row ->
|
items(rows.asReversed(), key = { it.key }) { row ->
|
||||||
|
val bounds = remember { RowBounds() }
|
||||||
|
Box(
|
||||||
|
Modifier.onGloballyPositioned {
|
||||||
|
bounds.top = it.positionInRoot().y
|
||||||
|
bounds.height = it.size.height.toFloat()
|
||||||
|
}
|
||||||
|
.holdTopEdge(row.key, topEdgeHeld) { grew ->
|
||||||
|
// Requested rather than scrolled. Scrolling forces a remeasure,
|
||||||
|
// and forcing one from inside a measure throws; this is the form
|
||||||
|
// built to be asked for during layout and applied in that pass.
|
||||||
|
listState.requestScrollToItem(
|
||||||
|
listState.firstVisibleItemIndex,
|
||||||
|
listState.firstVisibleItemScrollOffset + grew,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
when (row) {
|
when (row) {
|
||||||
is TranscriptRow.Tools ->
|
is TranscriptRow.Tools ->
|
||||||
ToolGroup(
|
ToolGroup(
|
||||||
group = row,
|
group = row,
|
||||||
expanded = row.id in expandedGroups,
|
expanded = row.id in expandedGroups,
|
||||||
onToggle = {
|
onToggle = { at ->
|
||||||
|
toggleAnchored(row.key, bounds, at) {
|
||||||
expandedGroups =
|
expandedGroups =
|
||||||
if (row.id in expandedGroups) expandedGroups - row.id
|
if (row.id in expandedGroups)
|
||||||
|
expandedGroups - row.id
|
||||||
else expandedGroups + row.id
|
else expandedGroups + row.id
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isToolExpanded = { it in expandedTools },
|
isToolExpanded = { it in expandedTools },
|
||||||
onToolToggle = { id ->
|
// Anchored on the group, not the call: opening one call makes
|
||||||
|
// the whole group taller, and the heading the reader is under
|
||||||
|
// is the group's.
|
||||||
|
onToolToggle = { id, at ->
|
||||||
|
toggleAnchored(row.key, bounds, at) {
|
||||||
expandedTools =
|
expandedTools =
|
||||||
if (id in expandedTools) expandedTools - id
|
if (id in expandedTools) expandedTools - id
|
||||||
else expandedTools + id
|
else expandedTools + id
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onAnswer = { questionId, answers ->
|
onAnswer = { questionId, answers ->
|
||||||
act {
|
act {
|
||||||
answerQuestion(settings, summary.id, questionId, answers)
|
answerQuestion(
|
||||||
|
settings,
|
||||||
|
summary.id,
|
||||||
|
questionId,
|
||||||
|
answers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image = { ref -> SessionImage(settings, summary.id, ref) },
|
image = { ref -> SessionImage(settings, summary.id, ref) },
|
||||||
@@ -1157,11 +1274,13 @@ fun SessionScreen(
|
|||||||
ToolCard(
|
ToolCard(
|
||||||
tool = item,
|
tool = item,
|
||||||
expanded = item.id in expandedTools,
|
expanded = item.id in expandedTools,
|
||||||
onToggle = {
|
onToggle = { at ->
|
||||||
|
toggleAnchored(row.key, bounds, at) {
|
||||||
expandedTools =
|
expandedTools =
|
||||||
if (item.id in expandedTools)
|
if (item.id in expandedTools)
|
||||||
expandedTools - item.id
|
expandedTools - item.id
|
||||||
else expandedTools + item.id
|
else expandedTools + item.id
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onAnswer = { questionId, answers ->
|
onAnswer = { questionId, answers ->
|
||||||
act {
|
act {
|
||||||
@@ -1173,12 +1292,19 @@ fun SessionScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image = { ref -> SessionImage(settings, summary.id, ref) },
|
image = { ref ->
|
||||||
|
SessionImage(settings, summary.id, ref)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
is TranscriptItem.QuestionCard ->
|
is TranscriptItem.QuestionCard ->
|
||||||
QuestionRow(item) { answers ->
|
QuestionRow(item) { answers ->
|
||||||
act {
|
act {
|
||||||
answerQuestion(settings, summary.id, item.id, answers)
|
answerQuestion(
|
||||||
|
settings,
|
||||||
|
summary.id,
|
||||||
|
item.id,
|
||||||
|
answers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is TranscriptItem.ErrorMsg ->
|
is TranscriptItem.ErrorMsg ->
|
||||||
@@ -1202,17 +1328,20 @@ fun SessionScreen(
|
|||||||
PeerMessageRow(
|
PeerMessageRow(
|
||||||
item = item,
|
item = item,
|
||||||
expanded = item.seq in expandedNotes,
|
expanded = item.seq in expandedNotes,
|
||||||
onToggle = {
|
onToggle = { at ->
|
||||||
|
toggleAnchored(row.key, bounds, at) {
|
||||||
expandedNotes =
|
expandedNotes =
|
||||||
if (item.seq in expandedNotes)
|
if (item.seq in expandedNotes)
|
||||||
expandedNotes - item.seq
|
expandedNotes - item.seq
|
||||||
else expandedNotes + item.seq
|
else expandedNotes + item.seq
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only while the newest message is off-screen. Reading back
|
// Only while the newest message is off-screen. Reading back
|
||||||
// through a conversation is a place to be, not a state to be
|
// through a conversation is a place to be, not a state to be
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.example.aiapp
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -18,6 +20,10 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -99,6 +105,38 @@ fun groupToolRuns(items: List<TranscriptItem>): List<TranscriptRow> {
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Where a click went down, and what it went down on. See [clickableAt]. */
|
||||||
|
private class TapPoint {
|
||||||
|
var coords: LayoutCoordinates? = null
|
||||||
|
var y = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clickable, and tells the click where on the screen the finger went down.
|
||||||
|
*
|
||||||
|
* A row holds still the end nearest the tap when it changes height, so the toggle has to say where
|
||||||
|
* it was touched. It cannot say which *end* it was: a group is one row with a control at each end
|
||||||
|
* and a call in the middle, and only the row knows where its own ends are. So this reports a
|
||||||
|
* position in root coordinates and leaves the meaning to whoever owns the row.
|
||||||
|
*
|
||||||
|
* Built on `clickable` rather than replacing it, because `clickable` is what draws the ripple and
|
||||||
|
* what puts a click action in front of assistive technology. The position is read on the initial
|
||||||
|
* pass and nothing is consumed, so the click still happens exactly as it would have.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Modifier.clickableAt(onClick: (Float) -> Unit): Modifier {
|
||||||
|
val tap = remember { TapPoint() }
|
||||||
|
return onGloballyPositioned { tap.coords = it }
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val down =
|
||||||
|
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||||
|
tap.y = tap.coords?.localToRoot(down.position)?.y ?: 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clickable { onClick(tap.y) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Several calls under one heading, closed until somebody asks.
|
* Several calls under one heading, closed until somebody asks.
|
||||||
*
|
*
|
||||||
@@ -113,14 +151,17 @@ fun groupToolRuns(items: List<TranscriptItem>): List<TranscriptRow> {
|
|||||||
fun ToolGroup(
|
fun ToolGroup(
|
||||||
group: TranscriptRow.Tools,
|
group: TranscriptRow.Tools,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onToggle: () -> Unit,
|
/**
|
||||||
|
* Told where it was pressed, because this row has a control at each end -- see [clickableAt].
|
||||||
|
*/
|
||||||
|
onToggle: (Float) -> Unit,
|
||||||
isToolExpanded: (String) -> Boolean,
|
isToolExpanded: (String) -> Boolean,
|
||||||
onToolToggle: (String) -> Unit,
|
onToolToggle: (String, Float) -> Unit,
|
||||||
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
||||||
image: @Composable (String) -> Unit,
|
image: @Composable (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
Card(Modifier.fillMaxWidth().clickable(onClick = onToggle)) {
|
Card(Modifier.fillMaxWidth().clickableAt(onToggle)) {
|
||||||
Text(
|
Text(
|
||||||
"Called ${group.calls.size} tools",
|
"Called ${group.calls.size} tools",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -133,28 +174,30 @@ fun ToolGroup(
|
|||||||
Text(
|
Text(
|
||||||
"Called ${group.calls.size} tools",
|
"Called ${group.calls.size} tools",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
modifier = Modifier.fillMaxWidth().clickable(onClick = onToggle).padding(12.dp),
|
modifier = Modifier.fillMaxWidth().clickableAt(onToggle).padding(12.dp),
|
||||||
)
|
)
|
||||||
group.calls.forEach { call ->
|
group.calls.forEach { call ->
|
||||||
ToolCard(
|
ToolCard(
|
||||||
tool = call,
|
tool = call,
|
||||||
expanded = isToolExpanded(call.id),
|
expanded = isToolExpanded(call.id),
|
||||||
onToggle = { onToolToggle(call.id) },
|
onToggle = { at -> onToolToggle(call.id, at) },
|
||||||
onAnswer = onAnswer,
|
onAnswer = onAnswer,
|
||||||
image = image,
|
image = image,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Shutting it from here anchors the other end: the reader is at the bottom of a long
|
||||||
|
// group, and what they are looking at is what follows it.
|
||||||
CollapseBar(onToggle)
|
CollapseBar(onToggle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The bottom half of a group's toggle: an arrow back up to its heading. */
|
/** The bottom half of a group's toggle: an arrow back up to its heading. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun CollapseBar(onToggle: () -> Unit) {
|
private fun CollapseBar(onToggle: (Float) -> Unit) {
|
||||||
val colour = MaterialTheme.colorScheme.onSurfaceVariant
|
val colour = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
.clickable(onClick = onToggle)
|
.clickableAt(onToggle)
|
||||||
.semantics { contentDescription = "Collapse these tool calls" }
|
.semantics { contentDescription = "Collapse these tool calls" }
|
||||||
.padding(vertical = 10.dp),
|
.padding(vertical = 10.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
@@ -181,14 +224,14 @@ private fun CollapseBar(onToggle: () -> Unit) {
|
|||||||
fun ToolCard(
|
fun ToolCard(
|
||||||
tool: TranscriptItem.ToolRun,
|
tool: TranscriptItem.ToolRun,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onToggle: () -> Unit,
|
onToggle: (Float) -> Unit,
|
||||||
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
onAnswer: (questionId: String, answers: List<String>) -> Unit,
|
||||||
image: @Composable (String) -> Unit = {},
|
image: @Composable (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val parsed = remember(tool.tool, tool.input) { parseToolInput(tool.tool, tool.input) }
|
val parsed = remember(tool.tool, tool.input) { parseToolInput(tool.tool, tool.input) }
|
||||||
val deciding = tool.asks.any { it.answers.isEmpty() }
|
val deciding = tool.asks.any { it.answers.isEmpty() }
|
||||||
val open = expanded || deciding
|
val open = expanded || deciding
|
||||||
Card(Modifier.fillMaxWidth().clickable(onClick = onToggle)) {
|
Card(Modifier.fillMaxWidth().clickableAt(onToggle)) {
|
||||||
Column(Modifier.padding(12.dp)) {
|
Column(Modifier.padding(12.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(tool.tool, style = MaterialTheme.typography.titleSmall)
|
Text(tool.tool, style = MaterialTheme.typography.titleSmall)
|
||||||
|
|||||||
Reference in new issue
Block a user