Merge branch 'main' of git.arirex.me:iris/ai-app
This commit is contained in:
commit
257f4c85c1
2 files changed
+36
-23
No files matched your search
@@ -2,6 +2,8 @@ package com.example.aiapp
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
@@ -11,7 +13,10 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import com.mikepenz.markdown.m3.markdownColor
|
||||
import com.mikepenz.markdown.m3.markdownTypography
|
||||
import com.mikepenz.markdown.model.State
|
||||
import com.mikepenz.markdown.model.parseMarkdown
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* An assistant's reply, rendered as the markdown it is written in.
|
||||
@@ -26,20 +31,7 @@ import com.mikepenz.markdown.model.parseMarkdown
|
||||
@Composable
|
||||
fun MarkdownText(text: String, modifier: Modifier = Modifier) {
|
||||
val body = MaterialTheme.typography.bodyLarge
|
||||
// Parsed here, in the composition, rather than by the overload that takes the text itself.
|
||||
// That one parses in a coroutine and draws an empty loading slot until the result arrives --
|
||||
// so a row is measured at nothing before it is measured at its real height, and the
|
||||
// transcript above it collapses and springs back. Seen with five replies on screen at once,
|
||||
// every one of them blank, the whole conversation shrunk to fit a single screen; a moment
|
||||
// later it was all there again. That is the "skipping up and down" this list must never do,
|
||||
// and no amount of scroll anchoring can survive a row that lies about its height first.
|
||||
//
|
||||
// The cost is the parse on the main thread, which is the trade being made deliberately: a
|
||||
// few milliseconds of work at the moment a row is composed, against a layout that is wrong
|
||||
// every time one is. If a very long reply ever makes that visible, the answer is the
|
||||
// renderer's streaming state -- which parses incrementally -- and not going back to a
|
||||
// placeholder with no height.
|
||||
val parsed = remember(text) { parseMarkdown(text) }
|
||||
val parsed = parsedMarkdown(text)
|
||||
Markdown(
|
||||
parsed,
|
||||
colors =
|
||||
@@ -112,3 +104,33 @@ fun MarkdownText(text: String, modifier: Modifier = Modifier) {
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [text] parsed: on the composing thread the first time this row is drawn, and off it every time
|
||||
* afterwards.
|
||||
*
|
||||
* The first parse has to be inline. The renderer's own asynchronous path draws an empty loading
|
||||
* slot until its result arrives, so a row is measured at nothing before it is measured at its real
|
||||
* height, and the transcript above it collapses and springs back. Seen with five replies on screen
|
||||
* at once, every one of them blank, the whole conversation shrunk to fit a single screen; a moment
|
||||
* later it was all there again. That is the "skipping up and down" this list must never do, and no
|
||||
* amount of scroll anchoring can survive a row that lies about its height first.
|
||||
*
|
||||
* Every parse *after* the first is a different case, and it is the one that was costing: a reply
|
||||
* arrives as hundreds of deltas, each one re-parsing the whole message it has grown into. Measured
|
||||
* against `/stream 200` on the emulator, that was fifty-eight parses and 78ms of main-thread work
|
||||
* in three seconds, with single parses reaching 7ms -- most of a frame at 60Hz and more than one at
|
||||
* 120. Those go to a background thread, and the row keeps drawing the parse it already has until
|
||||
* the new one lands, so there is never a frame without a height. What is on screen is always a
|
||||
* real prefix of the reply rather than a guess at it; it is simply one parse behind.
|
||||
*/
|
||||
@Composable
|
||||
private fun parsedMarkdown(text: String): State {
|
||||
// The text each parse came from, so the first composition's is not immediately repeated.
|
||||
val parsed = remember { mutableStateOf(text to parseMarkdown(text)) }
|
||||
LaunchedEffect(text) {
|
||||
if (parsed.value.first == text) return@LaunchedEffect
|
||||
parsed.value = text to withContext(Dispatchers.Default) { parseMarkdown(text) }
|
||||
}
|
||||
return parsed.value.second
|
||||
}
|
||||
@@ -601,9 +601,6 @@ fun SessionScreen(
|
||||
// screen starts with the end of the conversation and fetches earlier
|
||||
// pages only when somebody scrolls to them.
|
||||
var oldestSeq by remember { mutableLongStateOf(0L) }
|
||||
// Every transcript event loaded, in order, beside the rows they folded
|
||||
// into. See `apply`.
|
||||
var loaded by remember { mutableStateOf(listOf<SessionEvent>()) }
|
||||
// Sent, but not yet read by the session -- which is when the backend
|
||||
// records it and it comes back as a row. Until then it is drawn below
|
||||
// the working indicator, because that is where it is in the session's
|
||||
@@ -686,11 +683,6 @@ fun SessionScreen(
|
||||
if (event is SessionEvent.CommandSent) {
|
||||
waitingCommands = waitingCommands.filterNot { it.first == event.id }
|
||||
}
|
||||
// Kept as well as folded. Folding is one-way -- a tool's
|
||||
// start and end become one row -- so a page arriving in
|
||||
// front of what is already here cannot be stitched on
|
||||
// without the events themselves.
|
||||
loaded = loaded + event
|
||||
items = foldEvent(items, entry)
|
||||
}
|
||||
|
||||
@@ -830,7 +822,6 @@ fun SessionScreen(
|
||||
// screen -- `apply` refills them, and scrolling
|
||||
// up pages the rest back in as it always does.
|
||||
items = listOf()
|
||||
loaded = listOf()
|
||||
held = listOf()
|
||||
oldestSeq = 0L
|
||||
moreHistory = true
|
||||
|
||||
Reference in new issue
Block a user