Skip to content

Commit 8ca98d7

Browse files
Updated llama.cpp to b6c83aa and overhauled the demo app UI/UX.
* Updated llama.cpp to b6c83aad55a4ce17ec96fced7770cd1be8758193 * Refactored `ModelZoo` with new lightweight models (SmolLM2, Qwen2.5, DeepSeek-R1 Distill) and optimized inference configurations * Added support for `defaultSystemPrompt` per model in `Model` data class * Improved `ChatScreen` with expandable thinking blocks for reasoning models and "keep screen on" flag * Implemented custom navigation transitions (fade, scale, slide, parallax) in `AppTransitions` * Refactored `RootContainer` status bar with a dedicated loading progress bar and simplified state display * Modularized `ConversationsScreen` and `ChatScreen` into smaller, reusable Composable components * Added new utility methods to `ResourceState` for filtering and mapping loading/failure states * Renamed various drawable resources to follow standard naming conventions (removed `px` suffix)
1 parent f1a9e5b commit 8ca98d7

21 files changed

Lines changed: 609 additions & 259 deletions

File tree

app/src/main/java/com/suhel/llamabro/demo/model/Model.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import com.suhel.llamabro.sdk.model.PromptFormat
66
data class Model(
77
val id: String,
88
val name: String,
9-
val description: String?,
9+
val description: String? = null,
1010
val downloadUrl: String,
1111
val promptFormat: PromptFormat,
1212
val defaultInferenceConfig: InferenceConfig,
13+
val defaultSystemPrompt: String? = null
1314
)

app/src/main/java/com/suhel/llamabro/demo/model/ModelZoo.kt

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,94 @@ import com.suhel.llamabro.sdk.model.PromptFormats
55

66
val ModelZoo = listOf(
77
Model(
8-
id = "gemma-3n-2b",
9-
name = "Gemma 3n 2B",
10-
description = "Google’s Gemma 3n multimodal model handles image, audio, video, and text inputs. Available in 2B and 4B sizes, it supports 140 languages for text and multimodal tasks.",
11-
downloadUrl = "https://huggingface.co/unsloth/gemma-3n-E2B-it-GGUF/resolve/main/gemma-3n-E2B-it-Q4_K_M.gguf",
12-
promptFormat = PromptFormats.Gemma3,
8+
id = "smollm2-135m-instruct",
9+
name = "SmolLM2 135M Instruct",
10+
description = "Ultra-lightweight model for absolute maximum tokens-per-second. Perfect for baseline speed tests.",
11+
downloadUrl = "https://huggingface.co/unsloth/SmolLM2-135M-Instruct-GGUF/resolve/main/SmolLM2-135M-Instruct-F16.gguf",
12+
promptFormat = PromptFormats.ChatML,
1313
defaultInferenceConfig = InferenceConfig(
14-
temperature = 1.0f,
15-
topK = 64,
16-
minP = 0.01f,
17-
topP = 0.95f,
14+
temperature = 0.6f,
15+
repeatPenalty = 1.15f,
16+
presencePenalty = 0.15f,
17+
minP = 0.05f,
18+
topP = 0.9f,
19+
topK = 40,
1820
)
1921
),
20-
2122
Model(
22-
id = "qwen3.5-4b",
23-
name = "Qwen3.5 4B",
24-
description = "Qwen3.5 is Alibaba’s new model family. The multimodal hybrid reasoning LLMs deliver the strongest performances for their sizes",
25-
downloadUrl = "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf",
23+
id = "smollm2-360m-instruct",
24+
name = "SmolLM2 360M Instruct",
25+
description = "Highly efficient sub-0.5B model balancing sheer speed with improved coherence.",
26+
downloadUrl = "https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct-GGUF/resolve/main/smollm2-360m-instruct-q8_0.gguf",
2627
promptFormat = PromptFormats.ChatML,
2728
defaultInferenceConfig = InferenceConfig(
28-
temperature = 1.0f,
29-
topP = 0.95f,
30-
topK = 20,
31-
minP = 0.0f,
32-
presencePenalty = 1.5f,
33-
repeatPenalty = 1.0f,
29+
temperature = 0.7f,
30+
repeatPenalty = 1.15f,
31+
presencePenalty = 0.15f,
32+
minP = 0.05f,
33+
topP = 0.9f,
34+
topK = 40,
35+
)
36+
),
37+
Model(
38+
id = "qwen2.5-0.5b-instruct",
39+
name = "Qwen2.5 0.5B Instruct",
40+
description = "Exceptional speed with strong multilingual support and structured JSON formatting capabilities.",
41+
downloadUrl = "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q8_0.gguf",
42+
promptFormat = PromptFormats.ChatML,
43+
defaultInferenceConfig = InferenceConfig(
44+
temperature = 0.7f,
45+
repeatPenalty = 1.05f,
46+
presencePenalty = 0.15f,
47+
minP = 0.1f,
48+
topP = 0.8f,
49+
topK = 40,
3450
)
3551
),
36-
3752
Model(
3853
id = "llama-3.2-1b-instruct",
39-
name = "Llama 3.2 1B Instruct",
40-
description = "The Llama 3.2 collection of multilingual large language models (LLMs) is a collection of pretrained and instruction-tuned generative models.",
54+
name = "Llama-3.2 1B Instruct",
55+
description = "Meta's highly optimized 1B mobile model. The industry standard for reliable on-device chat.",
4156
downloadUrl = "https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/resolve/main/Llama-3.2-1B-Instruct-Q5_K_M.gguf",
4257
promptFormat = PromptFormats.Llama3,
4358
defaultInferenceConfig = InferenceConfig(
4459
temperature = 0.6f,
60+
repeatPenalty = 1.1f,
61+
presencePenalty = 0.15f,
62+
minP = 0.05f,
63+
topP = 0.9f,
64+
topK = 40,
65+
)
66+
),
67+
Model(
68+
id = "deepseek-r1-distill-qwen-1.5b",
69+
name = "DeepSeek-R1 1.5B (Distilled)",
70+
description = "Advanced reasoning capabilities on-device. Uses chain-of-thought processing.",
71+
downloadUrl = "https://huggingface.co/unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q8_0.gguf",
72+
promptFormat = PromptFormats.ChatML,
73+
defaultInferenceConfig = InferenceConfig(
74+
temperature = 0.6f,
75+
repeatPenalty = 1.0f,
76+
presencePenalty = 0.0f,
77+
minP = null,
78+
topP = 0.95f,
79+
topK = 40,
80+
),
81+
defaultSystemPrompt = "You are a helpful and harmless assistant. You are Llama Bro."
82+
),
83+
Model(
84+
id = "smollm2-1.7b-instruct",
85+
name = "SmolLM2 1.7B Instruct",
86+
description = "High-quality, nuanced text generation that punches above its weight class.",
87+
downloadUrl = "https://huggingface.co/bartowski/SmolLM2-1.7B-Instruct-GGUF/resolve/main/SmolLM2-1.7B-Instruct-Q5_K_M.gguf",
88+
promptFormat = PromptFormats.ChatML,
89+
defaultInferenceConfig = InferenceConfig(
90+
temperature = 0.7f,
91+
repeatPenalty = 1.1f,
92+
presencePenalty = 0.15f,
93+
minP = 0.1f,
4594
topP = 0.9f,
46-
topK = 50,
47-
repeatPenalty = 1.2f,
95+
topK = 40,
4896
)
4997
)
5098
)

app/src/main/java/com/suhel/llamabro/demo/ui/AppScaffold.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import com.suhel.llamabro.demo.R
2424
@Composable
2525
fun AppScaffold(
2626
title: String,
27+
modifier: Modifier = Modifier,
2728
onBack: (() -> Unit)? = null,
2829
floatingActionButton: @Composable () -> Unit = {},
2930
content: @Composable ColumnScope.() -> Unit
3031
) {
3132
Scaffold(
33+
modifier = modifier,
3234
topBar = {
3335
TopAppBar(
3436
title = {
@@ -38,7 +40,7 @@ fun AppScaffold(
3840
if (onBack != null) {
3941
IconButton(onBack) {
4042
Icon(
41-
painter = painterResource(R.drawable.arrow_back_24px),
43+
painter = painterResource(R.drawable.arrow_back_24),
4244
contentDescription = "Back"
4345
)
4446
}

app/src/main/java/com/suhel/llamabro/demo/ui/screens/chat/ChatScreen.kt

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.suhel.llamabro.demo.ui.screens.chat
22

33
import androidx.compose.animation.AnimatedContent
4+
import androidx.compose.animation.animateContentSize
5+
import androidx.compose.foundation.Image
46
import androidx.compose.foundation.background
7+
import androidx.compose.foundation.clickable
58
import androidx.compose.foundation.layout.Arrangement
69
import androidx.compose.foundation.layout.Box
710
import androidx.compose.foundation.layout.Column
@@ -29,6 +32,9 @@ import androidx.compose.runtime.remember
2932
import androidx.compose.runtime.setValue
3033
import androidx.compose.ui.Alignment
3134
import androidx.compose.ui.Modifier
35+
import androidx.compose.ui.draw.clip
36+
import androidx.compose.ui.draw.rotate
37+
import androidx.compose.ui.keepScreenOn
3238
import androidx.compose.ui.res.painterResource
3339
import androidx.compose.ui.text.input.KeyboardCapitalization
3440
import androidx.compose.ui.unit.dp
@@ -52,6 +58,7 @@ fun ChatScreen(
5258
) {
5359
AppScaffold(
5460
title = "Chat",
61+
modifier = Modifier.keepScreenOn(),
5562
onBack = onBack
5663
) {
5764
val messages = viewModel.messages.collectAsLazyPagingItems()
@@ -160,12 +167,12 @@ private fun InputBar(
160167
) { generating ->
161168
if (generating) {
162169
Icon(
163-
painter = painterResource(R.drawable.stop_circle_24px),
170+
painter = painterResource(R.drawable.stop_circle_24),
164171
contentDescription = "Stop"
165172
)
166173
} else {
167174
Icon(
168-
painter = painterResource(R.drawable.arrow_circle_up_24px),
175+
painter = painterResource(R.drawable.arrow_circle_up_24),
169176
contentDescription = "Send"
170177
)
171178
}
@@ -186,56 +193,117 @@ private fun MessageBubble(message: UiChatMessage) {
186193
Column(
187194
modifier = if (isUser) Modifier.widthIn(max = 300.dp) else Modifier.fillMaxWidth(),
188195
horizontalAlignment = if (isUser) Alignment.End else Alignment.Start,
189-
verticalArrangement = Arrangement.spacedBy(8.dp)
196+
verticalArrangement = Arrangement.spacedBy(16.dp)
190197
) {
191-
if (message.isProcessing && (message.content.isNullOrBlank() && message.thinking.isNullOrBlank())) {
192-
Text(
193-
text = "Processing...",
194-
style = MaterialTheme.typography.bodySmall,
195-
color = MaterialTheme.colorScheme.onSurfaceVariant
196-
)
198+
if (message.isProcessing && message.content.isNullOrBlank() && message.thinking.isNullOrBlank()) {
199+
ProcessingIndicator()
197200
}
198201

199202
message.thinking?.let { thinkingText ->
200-
Text(
201-
text = thinkingText,
202-
style = MaterialTheme.typography.bodySmall,
203-
color = MaterialTheme.colorScheme.primaryFixedDim,
204-
)
203+
ExpandableThinkingBlock(thinkingText = thinkingText)
205204
}
206205

207206
message.content?.let { contentText ->
208207
if (isUser) {
209-
Box(
210-
modifier = Modifier
211-
.background(
212-
MaterialTheme.colorScheme.primary,
213-
MaterialTheme.shapes.medium
214-
)
215-
.padding(16.dp)
216-
) {
217-
Text(
218-
text = contentText,
219-
style = MaterialTheme.typography.bodyMedium,
220-
color = MaterialTheme.colorScheme.onPrimary
221-
)
222-
}
208+
UserMessageContent(contentText = contentText)
223209
} else {
224-
MarkdownText(
225-
markdown = contentText,
226-
style = MaterialTheme.typography.bodyMedium,
227-
modifier = Modifier.fillMaxWidth()
228-
)
210+
AssistantMessageContent(contentText = contentText)
229211
}
230212
}
231213

232214
if (!isUser && message.tokensPerSecond != null) {
233-
Text(
234-
text = "%.1f tok/s".format(message.tokensPerSecond),
235-
style = MaterialTheme.typography.labelSmall,
236-
color = MaterialTheme.colorScheme.onSurfaceVariant
237-
)
215+
GenerationMetrics(tokensPerSecond = message.tokensPerSecond)
238216
}
239217
}
240218
}
241219
}
220+
221+
@Composable
222+
private fun ProcessingIndicator() {
223+
Text(
224+
text = "Processing...",
225+
style = MaterialTheme.typography.bodySmall,
226+
color = MaterialTheme.colorScheme.onSurfaceVariant
227+
)
228+
}
229+
230+
@Composable
231+
private fun ExpandableThinkingBlock(thinkingText: String) {
232+
var isExpanded by remember { mutableStateOf(false) }
233+
234+
Column(
235+
modifier = Modifier
236+
.fillMaxWidth()
237+
.background(
238+
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
239+
MaterialTheme.shapes.small
240+
)
241+
.animateContentSize()
242+
) {
243+
Row(
244+
modifier = Modifier
245+
.fillMaxWidth()
246+
.clip(MaterialTheme.shapes.small)
247+
.clickable { isExpanded = !isExpanded }
248+
.padding(8.dp),
249+
horizontalArrangement = Arrangement.spacedBy(4.dp),
250+
verticalAlignment = Alignment.CenterVertically
251+
) {
252+
Text(
253+
text = if (isExpanded) "Hide thought process" else "Show thought process",
254+
style = MaterialTheme.typography.labelSmall,
255+
color = MaterialTheme.colorScheme.onSurfaceVariant,
256+
modifier = Modifier.weight(1f)
257+
)
258+
Image(
259+
painter = painterResource(R.drawable.keyboard_arrow_down_24),
260+
contentDescription = "chevron-thinking",
261+
modifier = Modifier.rotate(if (isExpanded) 180f else 0f)
262+
)
263+
}
264+
265+
if (isExpanded) {
266+
MarkdownText(
267+
markdown = thinkingText,
268+
style = MaterialTheme.typography.bodySmall,
269+
modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
270+
)
271+
}
272+
}
273+
}
274+
275+
@Composable
276+
private fun UserMessageContent(contentText: String) {
277+
Box(
278+
modifier = Modifier
279+
.background(
280+
MaterialTheme.colorScheme.primary,
281+
MaterialTheme.shapes.medium
282+
)
283+
.padding(16.dp)
284+
) {
285+
Text(
286+
text = contentText,
287+
style = MaterialTheme.typography.bodyMedium,
288+
color = MaterialTheme.colorScheme.onPrimary
289+
)
290+
}
291+
}
292+
293+
@Composable
294+
private fun AssistantMessageContent(contentText: String) {
295+
MarkdownText(
296+
markdown = contentText,
297+
style = MaterialTheme.typography.bodyMedium,
298+
modifier = Modifier.fillMaxWidth()
299+
)
300+
}
301+
302+
@Composable
303+
private fun GenerationMetrics(tokensPerSecond: Float) {
304+
Text(
305+
text = "%.1f tok/s".format(tokensPerSecond),
306+
style = MaterialTheme.typography.labelSmall,
307+
color = MaterialTheme.colorScheme.onSurfaceVariant
308+
)
309+
}

0 commit comments

Comments
 (0)