refactor(chat): tool display cleanup — split ChatBubble.kt god file, drop dead code (#762)
* refactor(chat): extract tool display subsystem from ChatBubble.kt ChatBubble.kt was a 2578-line god file. Split it into four files in the same package (zero import churn, no behavior change): - ChatBubble.kt: user/assistant/system bubbles + dispatch + shared helpers (formatTimestamp, buildHighlightedString, InlineAttachment) - ParsedToolData.kt: the parsed tool payload model - ToolResultParser.kt: parseToolOutput + per-tool formatters + the Gson-compat JSON shims and tolerant exit_code parser - ToolBubble.kt: ToolBubble + ExpandedToolContent + HeaderRow + SecurityRiskChip + CopyButton formatTimestamp became internal (shared with ToolBubble.kt). Verified: ktlintCheck + testDebugUnitTest green locally. * refactor(chat): drop dead ParsedToolData fields and unused shim Verified via grep: parsed.args / parsed.result / parsed.stderr / parsed.isRunning / parsed.toolName had zero readers across app/src (main + test + androidTest). toolName + isRunning are asserted by ToolBubbleParsingTest, so they stay; args/result/stderr were built by every formatter (21 construction sites) and never read — removing them also kills a JSON.stringify-per-tool-complete wasted map build. keySet() shim had no call sites. formatTodoToolOutput's args param became unused with the field removal — dropped it too. Verified: ktlintCheck + testDebugUnitTest green locally. * refactor(chat): drop unused lastSessionId and ToolBubble isDarkTheme param ChatScrollController.lastSessionId: introduced in #683, never read or written since (namefreq scan + git log -S confirm single occurrence at declaration). Session-change detection moved to lastTailKey/ lastMessageCount. ToolBubble.isDarkTheme: never read in the body (MaterialTheme + statusColors do the theming); single call site updated. Verified: ktlintCheck + compileDebugKotlin green locally. --------- Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
3a96edd4c1
commit
1daa03d686
5 files changed
+1875
-1867
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,20 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanly parsed representation of a tool call, extracted from the
|
||||||
|
* tool.complete JSON payload (which contains both args and result).
|
||||||
|
*/
|
||||||
|
data class ParsedToolData(
|
||||||
|
val toolName: String = "",
|
||||||
|
val isTerminal: Boolean = false,
|
||||||
|
val stdout: String? = null,
|
||||||
|
val exitCode: Int? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
val summaryText: String? = null,
|
||||||
|
val durationSec: Double? = null,
|
||||||
|
val mainOutput: String? = null,
|
||||||
|
val extraFields: Map<String, String> = emptyMap(),
|
||||||
|
val isRunning: Boolean = false,
|
||||||
|
val diffOutput: String? = null,
|
||||||
|
val diffPath: String? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,528 @@
|
|||||||
|
package com.m57.hermescontrol.ui.chat
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||||
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
import com.m57.hermescontrol.ui.chat.components.DiffViewCard
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExpandedToolContent(
|
||||||
|
parsed: ParsedToolData,
|
||||||
|
contentColor: Color,
|
||||||
|
statusColors: HermesStatusColors,
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
// Summary line at top of expanded view
|
||||||
|
if (parsed.summaryText != null) {
|
||||||
|
Text(
|
||||||
|
text = parsed.summaryText,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.7f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.isTerminal) {
|
||||||
|
// ── Terminal output ──
|
||||||
|
parsed.stdout?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.9f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
parsed.error?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_tool_execution_error, it),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = statusColors.error,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
parsed.exitCode?.let { code ->
|
||||||
|
if (code != 0) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_tool_exit_code, code),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = statusColors.error,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration footer for terminal
|
||||||
|
parsed.durationSec?.let { dur ->
|
||||||
|
Text(
|
||||||
|
text = "Duration: ${"%.1f".format(dur)}s",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.5f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ── Generic tool output / Diff view ──
|
||||||
|
if (parsed.diffOutput != null) {
|
||||||
|
DiffViewCard(
|
||||||
|
diffText = parsed.diffOutput,
|
||||||
|
filePath = parsed.diffPath,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
parsed.mainOutput?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.9f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
parsed.extraFields.forEach { (key, value) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$key:",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.6f),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.9f),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Duration footer
|
||||||
|
parsed.durationSec?.let { dur ->
|
||||||
|
Text(
|
||||||
|
text = "Duration: ${"%.1f".format(dur)}s",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.5f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ToolBubble(
|
||||||
|
message: ChatMessage,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var showRawJson by remember { mutableStateOf(false) }
|
||||||
|
val chipColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
val contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
val statusColors = LocalHermesStatusColors.current
|
||||||
|
|
||||||
|
val parsed =
|
||||||
|
remember(message.content, message.toolName, message.toolStatus) {
|
||||||
|
parseToolOutput(message.content, message.toolName, message.toolStatus == ToolStatus.RUNNING)
|
||||||
|
}
|
||||||
|
val config = ToolSchemaRegistry.getDisplayConfig(message.toolName)
|
||||||
|
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var showCopyButton by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Auto-dismiss copy button after 4 seconds
|
||||||
|
LaunchedEffect(showCopyButton) {
|
||||||
|
if (showCopyButton) {
|
||||||
|
delay(4000)
|
||||||
|
showCopyButton = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 1.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
onClick = { expanded = !expanded },
|
||||||
|
colors = CardDefaults.cardColors(containerColor = chipColor),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.animateContentSize()
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
) {
|
||||||
|
// ── Header row: icon + tool name ──
|
||||||
|
HeaderRow(message, config, contentColor, statusColors)
|
||||||
|
|
||||||
|
// ── Tool progress preview (tool.progress) ──
|
||||||
|
if (message.toolStatus == ToolStatus.RUNNING && !message.progressPreview.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = message.progressPreview,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.7f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 4.dp, start = 22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Security risk chip (tool.output_risk) ──
|
||||||
|
val riskData = message.toolOutputRiskData
|
||||||
|
if (riskData != null && (riskData.risk == "medium" || riskData.risk == "high" || riskData.redacted)) {
|
||||||
|
SecurityRiskChip(riskData, contentColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Summary line (always visible when collapsed) ──
|
||||||
|
if (!expanded && parsed?.summaryText != null) {
|
||||||
|
Text(
|
||||||
|
text = parsed.summaryText,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.7f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 4.dp, start = 22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Expanded content ──
|
||||||
|
if (expanded) {
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
|
||||||
|
if (showRawJson) {
|
||||||
|
// Raw JSON view — selectable + copy button
|
||||||
|
Box {
|
||||||
|
SelectionContainer {
|
||||||
|
Text(
|
||||||
|
text = message.content,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.8f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CopyButton(
|
||||||
|
visible = showCopyButton,
|
||||||
|
textToCopy = message.content,
|
||||||
|
onCopy = { showCopyButton = false },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = 8.dp, y = (-8).dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_tool_show_parsed),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.testTag("chat_tool_show_parsed")
|
||||||
|
.clickable(role = Role.Button) { showRawJson = false },
|
||||||
|
)
|
||||||
|
} else if (parsed != null) {
|
||||||
|
// Clean structured expanded view
|
||||||
|
Box {
|
||||||
|
SelectionContainer {
|
||||||
|
ExpandedToolContent(parsed, contentColor, statusColors)
|
||||||
|
}
|
||||||
|
CopyButton(
|
||||||
|
visible = showCopyButton,
|
||||||
|
textToCopy = message.content,
|
||||||
|
onCopy = { showCopyButton = false },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = 8.dp, y = (-8).dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_tool_show_raw),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.testTag("chat_tool_show_raw")
|
||||||
|
.clickable(role = Role.Button) { showRawJson = true },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Unparseable content — show raw JSON, selectable
|
||||||
|
Box {
|
||||||
|
SelectionContainer {
|
||||||
|
Text(
|
||||||
|
text = message.content,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = contentColor.copy(alpha = 0.8f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CopyButton(
|
||||||
|
visible = showCopyButton,
|
||||||
|
textToCopy = message.content,
|
||||||
|
onCopy = { showCopyButton = false },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.offset(x = 8.dp, y = (-8).dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Timestamp ──
|
||||||
|
Text(
|
||||||
|
text = formatTimestamp(message.timestamp, DateFormat.is24HourFormat(LocalContext.current)),
|
||||||
|
color = contentColor.copy(alpha = 0.5f),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.align(Alignment.End).padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HeaderRow(
|
||||||
|
message: ChatMessage,
|
||||||
|
config: ToolDisplayConfig,
|
||||||
|
contentColor: Color,
|
||||||
|
statusColors: HermesStatusColors,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
// Status icon or spinner
|
||||||
|
if (message.toolStatus == ToolStatus.RUNNING) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val icon =
|
||||||
|
when (message.toolStatus) {
|
||||||
|
ToolStatus.COMPLETED -> Icons.Filled.CheckCircle
|
||||||
|
ToolStatus.FAILED -> Icons.Filled.Error
|
||||||
|
else -> Icons.Filled.Build
|
||||||
|
}
|
||||||
|
val tint =
|
||||||
|
when (message.toolStatus) {
|
||||||
|
ToolStatus.COMPLETED -> statusColors.success
|
||||||
|
ToolStatus.FAILED -> statusColors.error
|
||||||
|
else -> contentColor.copy(alpha = 0.6f)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = message.toolName ?: stringResource(R.string.chat_tool_fallback),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelMedium.copy(
|
||||||
|
color = contentColor,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security risk chip for [tool.output_risk] events.
|
||||||
|
*
|
||||||
|
* Shows a compact ⚠ badge when the backend flagged tool output as risky.
|
||||||
|
* Renders in the tool card between the header row and the summary line.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun SecurityRiskChip(
|
||||||
|
riskData: ToolOutputRiskData,
|
||||||
|
contentColor: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val statusColors = LocalHermesStatusColors.current
|
||||||
|
val (chipColor, label) =
|
||||||
|
when {
|
||||||
|
riskData.risk == "high" -> statusColors.error to "Risky output"
|
||||||
|
riskData.risk == "medium" -> statusColors.warning to "Caution"
|
||||||
|
else -> statusColors.warning to "Redacted"
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.padding(top = 4.dp, start = 22.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "⚠",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = chipColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
),
|
||||||
|
color = chipColor,
|
||||||
|
)
|
||||||
|
if (riskData.redacted && (riskData.risk == "high" || riskData.risk == "medium")) {
|
||||||
|
Text(
|
||||||
|
text = "· redacted",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = chipColor.copy(alpha = 0.7f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CopyButton(
|
||||||
|
visible: Boolean,
|
||||||
|
textToCopy: String,
|
||||||
|
onCopy: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn() + scaleIn(),
|
||||||
|
exit = fadeOut() + scaleOut(),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
|
||||||
|
shadowElevation = 6.dp,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText(null, textToCopy)))
|
||||||
|
}
|
||||||
|
onCopy()
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.ContentCopy,
|
||||||
|
contentDescription = stringResource(R.string.content_desc_copy),
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -54,7 +54,6 @@ class ChatScrollController(
|
|||||||
var pendingCount by mutableStateOf(0)
|
var pendingCount by mutableStateOf(0)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var lastSessionId: String? = null
|
|
||||||
private var lastTailKey: Any? = null
|
private var lastTailKey: Any? = null
|
||||||
private var lastMessageCount: Int = 0
|
private var lastMessageCount: Int = 0
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user