fix(#205): fix typing effect by checking actual streamed word counts and default it to enabled (#246)
* fix(#205): fix typing effect by checking actual streamed word counts and default it to enabled * fix: correct import path of rememberSaveable to resolve build error * fix: make typingDelayMs reactive using rememberUpdatedState in StreamingBubbleWithTypingEffect * fix(#205): support message.delta event type to enable streaming text * feat(#205): increase typing effect delay slider maximum to 1000ms * fix(#205): continue animating completed streaming messages until all words are revealed * fix(#205): revert slider delay maximum value back to 100ms
This commit is contained in:
3 files changed
+42
-11
No files matched your search
@@ -35,7 +35,7 @@ object AuthManager {
|
||||
private const val DEFAULT_HOST = "127.0.0.1"
|
||||
private const val DEFAULT_PORT = 9119
|
||||
private const val DEFAULT_AUTO_RECONNECT = true
|
||||
private const val DEFAULT_TYPING_EFFECT_ENABLED = false
|
||||
private const val DEFAULT_TYPING_EFFECT_ENABLED = true
|
||||
private const val DEFAULT_TYPING_EFFECT_DELAY_MS = 30
|
||||
|
||||
@Volatile
|
||||
|
||||
@@ -51,7 +51,7 @@ object EventParser {
|
||||
WsEvent.MessageStart(sessionId)
|
||||
}
|
||||
|
||||
"message.token" -> {
|
||||
"message.token", "message.delta" -> {
|
||||
val token = payload?.get("text") as? String ?: ""
|
||||
WsEvent.MessageToken(token, sessionId)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -127,6 +128,7 @@ fun ChatScreen(
|
||||
}
|
||||
val scrollScope = rememberCoroutineScope()
|
||||
var inputText by rememberSaveable { mutableStateOf("") }
|
||||
var lastAnimatedMessageId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val context = LocalContext.current
|
||||
@@ -434,12 +436,28 @@ fun ChatScreen(
|
||||
state.currentSearchMatchIndex < state.searchMatchIndices.size &&
|
||||
state.searchMatchIndices[state.currentSearchMatchIndex] == index
|
||||
|
||||
ChatBubble(
|
||||
message = message,
|
||||
isDarkTheme = isDark,
|
||||
searchQuery = if (state.isSearchActive) state.searchQuery else "",
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
)
|
||||
val isLastMessage = index == state.messages.lastIndex
|
||||
val isAssistant = message.role == MessageRole.ASSISTANT
|
||||
val typingEnabled = AuthManager.isTypingEffectEnabled()
|
||||
val typingDelayMs = AuthManager.getTypingEffectDelayMs()
|
||||
|
||||
if (typingEnabled && isLastMessage && isAssistant && lastAnimatedMessageId != message.id) {
|
||||
StreamingBubbleWithTypingEffect(
|
||||
streaming = message,
|
||||
typingDelayMs = typingDelayMs,
|
||||
isDark = isDark,
|
||||
onAnimationComplete = {
|
||||
lastAnimatedMessageId = message.id
|
||||
},
|
||||
)
|
||||
} else {
|
||||
ChatBubble(
|
||||
message = message,
|
||||
isDarkTheme = isDark,
|
||||
searchQuery = if (state.isSearchActive) state.searchQuery else "",
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming message — rendered separately for O(1) updates
|
||||
@@ -1041,15 +1059,28 @@ private fun StreamingBubbleWithTypingEffect(
|
||||
streaming: ChatMessage,
|
||||
typingDelayMs: Int,
|
||||
isDark: Boolean,
|
||||
onAnimationComplete: () -> Unit = {},
|
||||
) {
|
||||
var visibleWordCount by remember { mutableIntStateOf(0) }
|
||||
val currentContent = rememberUpdatedState(streaming.content)
|
||||
val currentIsStreaming = rememberUpdatedState(streaming.isStreaming)
|
||||
val currentDelayMs = rememberUpdatedState(typingDelayMs)
|
||||
|
||||
// Timer that ticks at the configured delay, incrementing the visible word
|
||||
// count each tick. Stops ticking when streaming ends, then shows all words.
|
||||
LaunchedEffect(Unit) {
|
||||
while (streaming.isStreaming) {
|
||||
delay(typingDelayMs.toLong())
|
||||
visibleWordCount++
|
||||
while (true) {
|
||||
val words = currentContent.value.split(" ")
|
||||
if (visibleWordCount < words.size) {
|
||||
delay(currentDelayMs.value.toLong())
|
||||
visibleWordCount++
|
||||
} else {
|
||||
if (!currentIsStreaming.value) {
|
||||
onAnimationComplete()
|
||||
break
|
||||
}
|
||||
delay(10)
|
||||
}
|
||||
}
|
||||
visibleWordCount = Int.MAX_VALUE
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user