diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..2cb83ee 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 629fcc1..e60a72b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -39,11 +39,12 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
- jvmTarget = "11"
+ jvmTarget = "17"
}
buildFeatures {
compose = true
@@ -68,10 +69,14 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
- // LangChain4j Core Orchestration
- implementation("dev.langchain4j:langchain4j:0.29.1")
+ // LangChain4j BOM (Bill of Materials)
+ // This ensures all langchain4j modules use the same, compatible version.
+ implementation(platform("dev.langchain4j:langchain4j-bom:0.36.2"))
- // We use the OpenAI module because almost all local Android LLM runners
- // (like Llama.cpp) host a local server that mimics the OpenAI API format.
- implementation("dev.langchain4j:langchain4j-open-ai:0.29.1")
+ // Now, declare the modules you need without specifying the version.
+ implementation("dev.langchain4j:langchain4j")
+ implementation("dev.langchain4j:langchain4j-open-ai")
+
+
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 62c633e..617ee8c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,10 @@
+
+
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
+ MaterialTheme {
+ MainChatScreen()
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainChatScreen() {
+ // State management for the drawer and the chat
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+
+ var inputText by remember { mutableStateOf("") }
+
+ // We start with a dummy greeting message
+ var messages by remember {
+ mutableStateOf(listOf(ChatMessage("Hello! I am your local agent. How can I help?", false)))
+ }
+
+ // 2. The Slide-out Drawer Setup
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet {
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ "Alice Agent Configuration",
+ modifier = Modifier.padding(16.dp),
+ style = MaterialTheme.typography.titleLarge
+ )
+ Divider()
+
+ // Placeholder 1: Model Selection
+ NavigationDrawerItem(
+ label = { Text("Select LLM Model") },
+ selected = false,
+ icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") },
+ onClick = {
+ scope.launch { drawerState.close() }
+ // TODO: Navigate to Model Selection Screen
+ }
+ )
+
+ // Placeholder 2: Skills Menu
+ NavigationDrawerItem(
+ label = { Text("Skills Library") },
+ selected = false,
+ icon = { Icon(Icons.Default.Build, contentDescription = "Skills") },
+ onClick = {
+ scope.launch { drawerState.close() }
+ // TODO: Navigate to Skills Menu 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) {
+ 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")
+ }
}
}
}
}
}
+// 6. A helper composable to draw the chat bubbles
@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
+fun ChatBubble(message: ChatMessage) {
+ val alignment = if (message.isUser) Alignment.CenterEnd else Alignment.CenterStart
+ val color = if (message.isUser) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondaryContainer
+ val textColor = if (message.isUser) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSecondaryContainer
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- AliceTheme {
- Greeting("Android")
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ contentAlignment = alignment
+ ) {
+ Surface(
+ shape = RoundedCornerShape(16.dp),
+ color = color,
+ modifier = Modifier.widthIn(max = 280.dp)
+ ) {
+ Text(
+ text = message.text,
+ modifier = Modifier.padding(12.dp),
+ color = textColor
+ )
+ }
}
}
\ 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
new file mode 100644
index 0000000..58332d1
--- /dev/null
+++ b/app/src/main/java/net/mmanningau/alice/net/mmanningau/alice.kt
@@ -0,0 +1,16 @@
+package net.mmanningau.alice
+
+import android.app.Application
+import com.chaquo.python.Python
+import com.chaquo.python.android.AndroidPlatform
+
+class alice : Application() {
+ override fun onCreate() {
+ super.onCreate()
+
+ // This boots the Python environment the moment the app starts
+ if (!Python.isStarted()) {
+ Python.start(AndroidPlatform(this))
+ }
+ }
+}
\ No newline at end of file