Updated to now auto create the skills folder ready for the skills plugin. Also updated the UI to show this path and allow for "real" navigation between settings and the chat interface.

This commit is contained in:
2026-02-27 11:28:50 +11:00
parent 0340f80adb
commit 12c54e237b
3 changed files with 198 additions and 105 deletions

View File

@@ -1,29 +1,26 @@
package net.mmanningau.alice // Ensure this matches your package name!
package net.mmanningau.alice
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers //Added so that Kotlin run the query in background threads
import android.util.Log //Added to facilitate the logging while developing the app
// 1. A simple data class to hold our messages
data class ChatMessage(val text: String, val isUser: Boolean)
@@ -45,6 +42,7 @@ fun MainChatScreen() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var currentScreen by remember { mutableStateOf("Chat") }
var inputText by remember { mutableStateOf("") }
// We start with a dummy greeting message
@@ -58,126 +56,129 @@ fun MainChatScreen() {
drawerContent = {
ModalDrawerSheet {
Spacer(modifier = Modifier.height(16.dp))
Text(
"Alice Agent Configuration",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge
)
Divider()
Text("Alice Configuration", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge)
HorizontalDivider()
// Placeholder 1: Model Selection
// Navigation item to go back to Chat
NavigationDrawerItem(
label = { Text("Select LLM Model") },
selected = false,
icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
label = { Text("Chat Interface") },
selected = currentScreen == "Chat",
icon = { Icon(Icons.Default.Send, contentDescription = "Chat") },
onClick = {
scope.launch { drawerState.close() }
// TODO: Navigate to Model Selection Screen
currentScreen = "Chat" // Swap screen
}
)
// Placeholder 2: Skills Menu
// Navigation item for Skills Library
NavigationDrawerItem(
label = { Text("Skills Library") },
selected = false,
selected = currentScreen == "Skills",
icon = { Icon(Icons.Default.Build, contentDescription = "Skills") },
onClick = {
scope.launch { drawerState.close() }
// TODO: Navigate to Skills Menu Screen
currentScreen = "Skills" // Swap screen
}
)
}
}
) {
// 3. The Main Screen Layout (Scaffold provides the top bar and body)
Scaffold(
topBar = {
TopAppBar(
title = { Text("Alice Agent") },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
// 4. The Chat Area
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// The scrolling list of messages
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 8.dp),
reverseLayout = true // Starts at the bottom like a real chat app
) {
// We reverse the list so the newest is at the bottom
items(messages.reversed()) { message ->
ChatBubble(message)
}
}
// 5. The Input Field
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Ask me to do something...") },
shape = RoundedCornerShape(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
FloatingActionButton(
onClick = {
if (inputText.isNotBlank()) {
val userText = inputText
// 1. Add user message to UI
messages = messages + ChatMessage(userText, true)
// 2. Clear input field
inputText = ""
// 3. Launch background thread to talk to LangChain4j
scope.launch(Dispatchers.IO) {
try {
// Send it to the local LLM!
val response = LlmManager.agent.chat(userText)
// Compose state automatically handles switching back to the main thread for UI updates
messages = messages + ChatMessage(response, false)
} catch (e: Exception) {
// THIS PRINTS THE REAL ERROR TO LOGCAT
Log.e("AliceNetwork", "LLM Connection Failed", e)
messages = messages + ChatMessage("Connection Error: Is the local LLM server running?", false)
}
}
// THE ROUTER LOGIC
if (currentScreen == "Chat") {
// 3. The Main Screen Layout (Scaffold provides the top bar and body)
Scaffold(
topBar = {
TopAppBar(
title = { Text("Alice Agent") },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
},
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(50)
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues -> // <-- Notice how this now safely wraps the Column below!
// 4. The Chat Area
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// The scrolling list of messages
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 8.dp),
reverseLayout = true // Starts at the bottom like a real chat app
) {
Icon(Icons.Default.Send, contentDescription = "Send")
// We reverse the list so the newest is at the bottom
items(messages.reversed()) { message ->
ChatBubble(message)
}
}
// 5. The Input Field
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Ask me to do something...") },
shape = RoundedCornerShape(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
FloatingActionButton(
onClick = {
if (inputText.isNotBlank()) {
val userText = inputText
// Add user message to UI
messages = messages + ChatMessage(userText, true)
// Clear input field
inputText = ""
// Launch background thread to talk to LangChain4j
scope.launch(Dispatchers.IO) {
try {
// Send it to the local LLM!
val response = LlmManager.agent.chat(userText)
// Compose state automatically handles switching back to the main thread for UI updates
messages = messages + ChatMessage(response, false)
} catch (e: Exception) {
Log.e("AliceNetwork", "LLM Connection Failed", e)
messages = messages + ChatMessage("Connection Error: Is the local LLM server running?", false)
}
}
}
},
containerColor = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(50)
) {
Icon(Icons.Default.Send, contentDescription = "Send")
}
}
}
}
} // <-- Scaffold closes here
} else if (currentScreen == "Skills") {
// LOAD THE NEW SCREEN
SkillsLibraryScreen(
onBackClicked = { currentScreen = "Chat" }
)
}
}
}
@@ -207,4 +208,62 @@ fun ChatBubble(message: ChatMessage) {
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SkillsLibraryScreen(onBackClicked: () -> Unit) {
// Grab the path from our Manager
val skillsPath = SkillManager.skillsDirectory?.absolutePath ?: "Path unavailable"
Scaffold(
topBar = {
TopAppBar(
title = { Text("Skills Library") },
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
titleContentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
Text(
text = "Local Skill Directory",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
// Displays the actual folder path on the device
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = skillsPath,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Drop your Python scripts (script.py) and JSON configurations (manifest.json) into subfolders within this directory to expand Alice's capabilities.",
style = MaterialTheme.typography.bodyMedium
)
}
}
}

View File

@@ -0,0 +1,32 @@
package net.mmanningau.alice
import android.content.Context
import android.util.Log
import java.io.File
object SkillManager {
// We hold the path here so the UI and LangChain4j can both access it instantly
var skillsDirectory: File? = null
private set
fun initialize(context: Context) {
// getExternalFilesDir(null) points to:
// /storage/emulated/0/Android/data/net.mmanningau.alice/files
// This is safe from Scoped Storage restrictions, but accessible via Android file managers.
val baseDir = context.getExternalFilesDir(null)
val skillsDir = File(baseDir, "Skills")
if (!skillsDir.exists()) {
val created = skillsDir.mkdirs()
if (created) {
Log.i("AliceSkills", "Created Skills directory at: ${skillsDir.absolutePath}")
} else {
Log.e("AliceSkills", "Failed to create Skills directory at: ${skillsDir.absolutePath}")
}
} else {
Log.i("AliceSkills", "Skills directory already exists at: ${skillsDir.absolutePath}")
}
skillsDirectory = skillsDir
}
}

View File

@@ -12,5 +12,7 @@ class alice : Application() {
if (!Python.isStarted()) {
Python.start(AndroidPlatform(this))
}
// ADD THIS: Create the skills folder and resolve the path immediately
SkillManager.initialize(this)
}
}