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