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:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
app/src/main/java/net/mmanningau/alice/SkillManager.kt
Normal file
32
app/src/main/java/net/mmanningau/alice/SkillManager.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user