From 12c54e237b29a169296fa080386b661e43d47347 Mon Sep 17 00:00:00 2001 From: mmanningau Date: Fri, 27 Feb 2026 11:28:50 +1100 Subject: [PATCH] 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. --- .../java/net/mmanningau/alice/MainActivity.kt | 269 +++++++++++------- .../java/net/mmanningau/alice/SkillManager.kt | 32 +++ .../mmanningau/alice/net/mmanningau/alice.kt | 2 + 3 files changed, 198 insertions(+), 105 deletions(-) create mode 100644 app/src/main/java/net/mmanningau/alice/SkillManager.kt diff --git a/app/src/main/java/net/mmanningau/alice/MainActivity.kt b/app/src/main/java/net/mmanningau/alice/MainActivity.kt index 5e2bbf0..92737d4 100644 --- a/app/src/main/java/net/mmanningau/alice/MainActivity.kt +++ b/app/src/main/java/net/mmanningau/alice/MainActivity.kt @@ -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 + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/mmanningau/alice/SkillManager.kt b/app/src/main/java/net/mmanningau/alice/SkillManager.kt new file mode 100644 index 0000000..6329c4f --- /dev/null +++ b/app/src/main/java/net/mmanningau/alice/SkillManager.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/mmanningau/alice/net/mmanningau/alice.kt b/app/src/main/java/net/mmanningau/alice/net/mmanningau/alice.kt index 58332d1..5694d7f 100644 --- a/app/src/main/java/net/mmanningau/alice/net/mmanningau/alice.kt +++ b/app/src/main/java/net/mmanningau/alice/net/mmanningau/alice.kt @@ -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) } } \ No newline at end of file