feat(chat): display completion timestamp beside copy button on assistant message (#1053)
This commit is contained in:
4 files changed
+53
-16
No files matched your search
@@ -66,6 +66,11 @@ data class ChatMessage(
|
|||||||
* them as centered timeline chips instead of user bubbles.
|
* them as centered timeline chips instead of user bubbles.
|
||||||
*/
|
*/
|
||||||
val displayKind: String? = null,
|
val displayKind: String? = null,
|
||||||
|
/**
|
||||||
|
* Timestamp (ms) when the assistant message finished streaming.
|
||||||
|
* When present, rendered at the bottom beside the copy button.
|
||||||
|
*/
|
||||||
|
val finishTimestamp: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2665,7 +2665,11 @@ class ChatViewModel(
|
|||||||
private fun sealStreamingMessageIfAny() {
|
private fun sealStreamingMessageIfAny() {
|
||||||
val streaming = _streamingState.value.streamingMessage ?: return
|
val streaming = _streamingState.value.streamingMessage ?: return
|
||||||
if (streaming.content.isBlank() && streaming.reasoningText.isBlank()) return
|
if (streaming.content.isBlank() && streaming.reasoningText.isBlank()) return
|
||||||
val finalized = streaming.copy(isStreaming = false)
|
val finalized =
|
||||||
|
streaming.copy(
|
||||||
|
isStreaming = false,
|
||||||
|
finishTimestamp = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
_uiState.update { it.copy(messages = (it.messages + finalized).dedupeById()) }
|
_uiState.update { it.copy(messages = (it.messages + finalized).dedupeById()) }
|
||||||
val sid = _uiState.value.currentSessionId
|
val sid = _uiState.value.currentSessionId
|
||||||
if (sid != null) {
|
if (sid != null) {
|
||||||
|
|||||||
@@ -206,7 +206,11 @@ object ChatWsEventReducer {
|
|||||||
var orphan: ChatMessage? = null
|
var orphan: ChatMessage? = null
|
||||||
val preState =
|
val preState =
|
||||||
if (streamingState.streamingMessage?.content?.isNotEmpty() == true) {
|
if (streamingState.streamingMessage?.content?.isNotEmpty() == true) {
|
||||||
val finalized = streamingState.streamingMessage.copy(isStreaming = false)
|
val finalized =
|
||||||
|
streamingState.streamingMessage.copy(
|
||||||
|
isStreaming = false,
|
||||||
|
finishTimestamp = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
orphan = finalized
|
orphan = finalized
|
||||||
state.copy(
|
state.copy(
|
||||||
messages = state.messages.upsertById(finalized),
|
messages = state.messages.upsertById(finalized),
|
||||||
@@ -369,10 +373,12 @@ object ChatWsEventReducer {
|
|||||||
content = text,
|
content = text,
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
reasoningText = reasoning,
|
reasoningText = reasoning,
|
||||||
|
finishTimestamp = System.currentTimeMillis(),
|
||||||
) ?: ChatMessage(
|
) ?: ChatMessage(
|
||||||
role = MessageRole.ASSISTANT,
|
role = MessageRole.ASSISTANT,
|
||||||
content = text,
|
content = text,
|
||||||
reasoningText = reasoning,
|
reasoningText = reasoning,
|
||||||
|
finishTimestamp = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
val effects = mutableListOf<ReducerEffect>()
|
val effects = mutableListOf<ReducerEffect>()
|
||||||
val sid = state.currentSessionId
|
val sid = state.currentSessionId
|
||||||
@@ -418,7 +424,12 @@ object ChatWsEventReducer {
|
|||||||
} else {
|
} else {
|
||||||
streaming.reasoningText
|
streaming.reasoningText
|
||||||
}
|
}
|
||||||
val msg = streaming.copy(isStreaming = false, reasoningText = reasoning)
|
val msg =
|
||||||
|
streaming.copy(
|
||||||
|
isStreaming = false,
|
||||||
|
reasoningText = reasoning,
|
||||||
|
finishTimestamp = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
val effects = mutableListOf<ReducerEffect>()
|
val effects = mutableListOf<ReducerEffect>()
|
||||||
val sid = state.currentSessionId
|
val sid = state.currentSessionId
|
||||||
if (sid != null) {
|
if (sid != null) {
|
||||||
@@ -481,6 +492,7 @@ object ChatWsEventReducer {
|
|||||||
streamingState.streamingMessage.copy(
|
streamingState.streamingMessage.copy(
|
||||||
isStreaming = false,
|
isStreaming = false,
|
||||||
reasoningText = reasoning,
|
reasoningText = reasoning,
|
||||||
|
finishTimestamp = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
orphanToPersist = finalized
|
orphanToPersist = finalized
|
||||||
state.copy(
|
state.copy(
|
||||||
|
|||||||
+29
-13
@@ -140,20 +140,36 @@ internal fun FullBleedAgentMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!message.isStreaming) {
|
if (!message.isStreaming) {
|
||||||
IconButton(
|
Row(
|
||||||
onClick = {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
scope.launch {
|
|
||||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, message.content)))
|
|
||||||
}
|
|
||||||
copied = true
|
|
||||||
},
|
|
||||||
modifier = Modifier.size(28.dp).testTag("fullbleed_copy"),
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
IconButton(
|
||||||
imageVector = if (copied) Icons.Filled.Check else Icons.Filled.ContentCopy,
|
onClick = {
|
||||||
contentDescription = stringResource(R.string.content_desc_copy),
|
scope.launch {
|
||||||
modifier = Modifier.size(14.dp),
|
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, message.content)))
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
}
|
||||||
|
copied = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(28.dp).testTag("fullbleed_copy"),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (copied) Icons.Filled.Check else Icons.Filled.ContentCopy,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_copy),
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
val finishTime = message.finishTimestamp ?: message.timestamp
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
com.m57.hermescontrol.ui.chat.formatTimestamp(
|
||||||
|
finishTime,
|
||||||
|
DateFormat.is24HourFormat(LocalContext.current),
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.testTag("fullbleed_finish_time"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user