feat: single-row input, pills inside card, new-chat button
ChatInput redesigned: single row (Plus | Text | Tune | Mic | Send). Pills hidden by default, expandable via tune icon with slide animation inside the same GlassSurface card. Shape lg (28dp) instead of pill. ModePillsRow moved from separate floating row into ChatInput as pillsContent lambda. Bottom dock scrim simplified. New Chat button (PenLine icon) added to top bar next to TokenBadge. LazyColumn bottom padding 260→140dp (dock is now smaller).
This commit is contained in:
parent
ad98b9f499
commit
4958ca8429
2 changed files with 113 additions and 63 deletions
|
|
@ -737,6 +737,7 @@ fun ChatInput(
|
|||
pendingFiles: List<PendingFile> = emptyList(),
|
||||
onAddClick: () -> Unit = {},
|
||||
onRemoveFile: (String) -> Unit = {},
|
||||
pillsContent: @Composable (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val cs = MaterialTheme.colorScheme
|
||||
|
|
@ -746,20 +747,20 @@ fun ChatInput(
|
|||
val hasContent = value.isNotBlank() || pendingFiles.any { it.uploaded != null }
|
||||
val canSend = enabled && hasContent && pendingFiles.none { it.uploading }
|
||||
|
||||
val iconBg = if (isDark) Color.White.copy(alpha = 0.07f) else Color.Black.copy(alpha = 0.05f)
|
||||
var showPills by remember { mutableStateOf(false) }
|
||||
|
||||
GlassSurface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
shape = KaizenShapes.pill,
|
||||
tintAlpha = if (isDark) 0.82f else 0.88f,
|
||||
shape = KaizenShapes.lg,
|
||||
tintAlpha = if (isDark) 0.84f else 0.90f,
|
||||
shadowStack = KaizenShadows.level3,
|
||||
cornerRadius = 32.dp,
|
||||
cornerRadius = 28.dp,
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 14.dp, vertical = 14.dp)) {
|
||||
Column(Modifier.padding(vertical = 10.dp)) {
|
||||
if (isRecording) {
|
||||
// Recording state — live waveform replaces the text field
|
||||
Column(Modifier.padding(horizontal = 14.dp)) {
|
||||
VoiceWaveform(amplitude = recordingAmplitude)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
|
@ -774,48 +775,84 @@ fun ChatInput(
|
|||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Expandable pills row
|
||||
if (pillsContent != null) {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = showPills,
|
||||
enter = androidx.compose.animation.expandVertically() + androidx.compose.animation.fadeIn(),
|
||||
exit = androidx.compose.animation.shrinkVertically() + androidx.compose.animation.fadeOut(),
|
||||
) {
|
||||
Column {
|
||||
pillsContent()
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Box(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 14.dp).height(0.5.dp)
|
||||
.background(cs.onSurfaceVariant.copy(alpha = 0.10f))
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingFiles.isNotEmpty()) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(bottom = 10.dp),
|
||||
Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(start = 14.dp, end = 14.dp, bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
pendingFiles.forEach { pf -> PendingFileChip(pf, onRemove = { onRemoveFile(pf.id) }) }
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxWidth().heightIn(min = 48.dp), contentAlignment = Alignment.CenterStart) {
|
||||
// Single-row input: Plus | TextField | Mic | Send/Call
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(38.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(21.dp))
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.weight(1f).heightIn(min = 38.dp).padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
if (value.isEmpty()) Text(
|
||||
stringResource(R.string.chat_input_placeholder),
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.45f),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.W300,
|
||||
)
|
||||
BasicTextField(
|
||||
value = value, onValueChange = onValueChange,
|
||||
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W300),
|
||||
cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(),
|
||||
cursorBrush = SolidColor(accent.primary), maxLines = 4, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (pillsContent != null) {
|
||||
Box(
|
||||
Modifier.size(42.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onAddClick),
|
||||
Modifier.size(38.dp).clip(KaizenShapes.circle).clickable { showPills = !showPills },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
Icon(
|
||||
KaizenIcons.SlidersHorizontal, null,
|
||||
tint = if (showPills) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.55f),
|
||||
modifier = Modifier.size(19.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
Modifier.size(42.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onMicClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Mic, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(
|
||||
Modifier.size(38.dp).clip(KaizenShapes.circle).clickable(onClick = onMicClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Mic, stringResource(R.string.chat_mic_permission), tint = cs.onSurfaceVariant, modifier = Modifier.size(21.dp))
|
||||
}
|
||||
|
||||
if (isStreaming) {
|
||||
StopButton(onClick = onStop)
|
||||
|
|
|
|||
|
|
@ -863,7 +863,7 @@ fun ChatScreen(
|
|||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier),
|
||||
contentPadding = PaddingValues(
|
||||
top = 100.dp,
|
||||
bottom = 260.dp,
|
||||
bottom = 140.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp
|
||||
),
|
||||
|
|
@ -1035,6 +1035,18 @@ fun ChatScreen(
|
|||
Spacer(Modifier.weight(1f))
|
||||
val ctxLen = models.find { it.id == displayModel }?.context_length ?: 1_000_000
|
||||
TokenBadge(used = usedTokens, contextLength = ctxLen)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
Modifier.size(36.dp).clip(KaizenShapes.circle)
|
||||
.clickable { haptics.tick(); newChat() },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
KaizenIcons.PenLine, null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(17.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1089,7 +1101,21 @@ fun ChatScreen(
|
|||
)
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
) {
|
||||
Spacer(Modifier.height(if (imeVisible) 6.dp else 24.dp))
|
||||
Spacer(Modifier.height(if (imeVisible) 4.dp else 12.dp))
|
||||
ChatInput(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
onSend = { send(input) },
|
||||
enabled = !isStreaming,
|
||||
isStreaming = isStreaming,
|
||||
onStop = { streamJob?.cancel() },
|
||||
isRecording = isRecording,
|
||||
recordingAmplitude = recordingAmplitude,
|
||||
onMicClick = { toggleRecording() },
|
||||
pendingFiles = pendingFiles,
|
||||
onAddClick = { showAttachMenu = !showAttachMenu },
|
||||
onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } },
|
||||
pillsContent = {
|
||||
ModePillsRow(
|
||||
activeMode = activeMode,
|
||||
onToggle = { mode ->
|
||||
|
|
@ -1104,20 +1130,7 @@ fun ChatScreen(
|
|||
webSearchProvider = webSearchProvider,
|
||||
onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() },
|
||||
)
|
||||
Spacer(Modifier.height(if (imeVisible) 6.dp else 10.dp))
|
||||
ChatInput(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
onSend = { send(input) },
|
||||
enabled = !isStreaming,
|
||||
isStreaming = isStreaming,
|
||||
onStop = { streamJob?.cancel() },
|
||||
isRecording = isRecording,
|
||||
recordingAmplitude = recordingAmplitude,
|
||||
onMicClick = { toggleRecording() },
|
||||
pendingFiles = pendingFiles,
|
||||
onAddClick = { showAttachMenu = !showAttachMenu },
|
||||
onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } },
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(if (imeVisible) 4.dp else 16.dp))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue