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