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,76 +747,112 @@ 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
|
||||
VoiceWaveform(amplitude = recordingAmplitude)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(
|
||||
Modifier.size(42.dp).clip(KaizenShapes.circle)
|
||||
.background(KaizenSemanticColors.error)
|
||||
.clickable(onClick = onMicClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Square, stringResource(R.string.chat_stop), tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
Column(Modifier.padding(horizontal = 14.dp)) {
|
||||
VoiceWaveform(amplitude = recordingAmplitude)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(
|
||||
Modifier.size(42.dp).clip(KaizenShapes.circle)
|
||||
.background(KaizenSemanticColors.error)
|
||||
.clickable(onClick = onMicClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(KaizenIcons.Square, stringResource(R.string.chat_stop), tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
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) {
|
||||
if (value.isEmpty()) Text(
|
||||
stringResource(R.string.chat_input_placeholder),
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Single-row input: Plus | TextField | Mic | Send/Call
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(42.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onAddClick),
|
||||
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(22.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))
|
||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(21.dp))
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
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.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 = 4, modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
if (pillsContent != null) {
|
||||
Box(
|
||||
Modifier.size(38.dp).clip(KaizenShapes.circle).clickable { showPills = !showPills },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
KaizenIcons.SlidersHorizontal, null,
|
||||
tint = if (showPills) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.55f),
|
||||
modifier = Modifier.size(19.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,22 +1101,7 @@ fun ChatScreen(
|
|||
)
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
) {
|
||||
Spacer(Modifier.height(if (imeVisible) 6.dp else 24.dp))
|
||||
ModePillsRow(
|
||||
activeMode = activeMode,
|
||||
onToggle = { mode ->
|
||||
activeMode = if (activeMode == mode) null else mode
|
||||
haptics.tick()
|
||||
},
|
||||
reasoningPreset = reasoningPreset,
|
||||
onReasoningChange = { reasoningPreset = it; haptics.tick() },
|
||||
samplingPrefs = samplingPrefs,
|
||||
onSamplingChange = { samplingPrefs = it },
|
||||
currentModelId = session.config?.model ?: "",
|
||||
webSearchProvider = webSearchProvider,
|
||||
onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() },
|
||||
)
|
||||
Spacer(Modifier.height(if (imeVisible) 6.dp else 10.dp))
|
||||
Spacer(Modifier.height(if (imeVisible) 4.dp else 12.dp))
|
||||
ChatInput(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
|
|
@ -1118,6 +1115,22 @@ fun ChatScreen(
|
|||
pendingFiles = pendingFiles,
|
||||
onAddClick = { showAttachMenu = !showAttachMenu },
|
||||
onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } },
|
||||
pillsContent = {
|
||||
ModePillsRow(
|
||||
activeMode = activeMode,
|
||||
onToggle = { mode ->
|
||||
activeMode = if (activeMode == mode) null else mode
|
||||
haptics.tick()
|
||||
},
|
||||
reasoningPreset = reasoningPreset,
|
||||
onReasoningChange = { reasoningPreset = it; haptics.tick() },
|
||||
samplingPrefs = samplingPrefs,
|
||||
onSamplingChange = { samplingPrefs = it },
|
||||
currentModelId = session.config?.model ?: "",
|
||||
webSearchProvider = webSearchProvider,
|
||||
onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() },
|
||||
)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(if (imeVisible) 4.dp else 16.dp))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue