Skip to content
This repository was archived by the owner on Apr 13, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<application
android:allowBackup="false"
android:supportsRtl="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import androidx.compose.runtime.MutableState
data class ChatMessage(
val message: MutableState<String>,
val type: Type,
val id: String
val id: String,
val isThinking: Boolean = false
){
enum class Type{
User, Bot
Expand Down
148 changes: 138 additions & 10 deletions androidApp/src/main/java/com/dilivva/inferkt/android/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,54 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.StartOffset
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Switch
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.dilivva.inferkt.setGlobalThinkingMode
import com.dilivva.inferkt.android.R

@Composable
fun ChatScreen() {
Expand All @@ -60,7 +83,13 @@ fun ChatScreen() {
viewModel.setModelPath(context, it)
}

Column(modifier = Modifier.fillMaxSize().background(Color.White).padding(16.dp)) {
Column(modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.imePadding()
.background(Color.White)
.padding(16.dp)) {
TopBar(viewModel)
Box(modifier = Modifier.weight(1f)) {
LazyColumn(
state = rememberLazyListState(),
Expand All @@ -69,8 +98,8 @@ fun ChatScreen() {
contentPadding = PaddingValues(vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
items(viewModel.messages, key = { it.id }) {
ChatItem(it)
items(viewModel.messages, key = { it.id }) { msg ->
ChatItem(msg)
}

}
Expand All @@ -93,6 +122,26 @@ fun ChatScreen() {

}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(viewModel: ChatViewModel) {
TopAppBar(
title = { Text(text = LocalContext.current.getString(R.string.app_name)) },
actions = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Switch(checked = viewModel.isThinkingEnabled, onCheckedChange = {
viewModel.isThinkingEnabled = it
try { setGlobalThinkingMode(it) } catch (_: Throwable) {}
})
Text("Thinking")
}
}
)
}

@Composable
private fun ChatItem(chatMessage: ChatMessage){
Column(modifier = Modifier.fillMaxWidth()) {
Expand All @@ -108,18 +157,94 @@ private fun ChatItem(chatMessage: ChatMessage){
}

ChatMessage.Type.Bot -> {
Text(
text = chatMessage.message.value,
color = Color.Black,
Column(
modifier = Modifier.align(Alignment.Start)
.background(Color.LightGray, RoundedCornerShape(8.dp))
.padding(8.dp)
)
.background(Color(0xFFF4F4F5), RoundedCornerShape(12.dp))
.padding(10.dp)
) {
// Collapsible thinking bubble or final answer bubble
if (chatMessage.isThinking) {
CollapsibleThinking(text = chatMessage.message.value)
} else if (chatMessage.message.value.isNotBlank()) {
val isStreaming = chatMessage.message.value.length < 100000
if (isStreaming) BasicText(text = chatMessage.message.value) else Text(text = chatMessage.message.value, color = Color.Black)
}
}
}
}
}
}

@Composable
private fun CollapsibleThinking(text: String) {
val expanded = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(Color(0xFFEFEFF1), RoundedCornerShape(10.dp))
.padding(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clickable { expanded.value = !expanded.value }
) {
Text("Thinking", color = Color.Gray)
Text(
text = if (expanded.value) "Hide" else "Show",
color = Color.Gray,
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(Color(0xFFDADAE0))
.padding(horizontal = 6.dp, vertical = 2.dp)
.padding(2.dp)
.clickable { expanded.value = !expanded.value }
)
}
if (expanded.value && text.isNotBlank()) {
BasicText(text = text)
}
}
}

@Composable
private fun ThinkingIndicator(label: String){
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Dot()
Dot(delayMs = 150)
Dot(delayMs = 300)
Text(
text = label,
color = Color.Gray,
modifier = Modifier.padding(start = 2.dp)
)
}
}

@Composable
private fun Dot(delayMs: Int = 0){
val transition = rememberInfiniteTransition()
val alpha = transition.animateFloat(
initialValue = 0.3f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 900, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
initialStartOffset = StartOffset(delayMs)
)
)
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(Color.Gray.copy(alpha = alpha.value))
)
}

@Composable
private fun ModelControl(onClick: () -> Unit){
Button(
Expand All @@ -132,7 +257,10 @@ private fun ModelControl(onClick: () -> Unit){

@Composable
private fun ChatControl(viewModel: ChatViewModel){
Row {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
viewModel.userMessage,
onValueChange = { viewModel.userMessage = it },
Expand Down
Loading